Adding
This commit is contained in:
+545
@@ -0,0 +1,545 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import * as reactRedux from "react-redux";
|
||||
import ActivityDetailPage from "./ActivityDetailPage";
|
||||
import ReactRouter from "react-router";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import Product2 from "../../shared/enumerations/Product";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityTransferState,
|
||||
ActivityType,
|
||||
IActivityStateChange,
|
||||
IActivityTransferAction,
|
||||
IAuditableActivity,
|
||||
SystemType
|
||||
} from "../../shared/models/Activity";
|
||||
import moment from "moment";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import { IDepartmentState } from "../../reducers/DepartmentReducer";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { getMockAppSettings } from "../../shared/testing/mocks/MockAppSettings";
|
||||
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const pageTranslate = Translate.Get.containers.activityDetailPage;
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
describe("ActivityDetailPage", () => {
|
||||
it("should display components if data is retrieved successfully", async () => {
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "1" });
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
const getActivitySpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"getViewableActivity"
|
||||
);
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
getActivitySpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
activity: activity(),
|
||||
authorized: {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
},
|
||||
transferAction: mockActivityTransferAction(),
|
||||
stateChanges: changes()
|
||||
}
|
||||
});
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<ActivityDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-details-components']").length
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should call the activity service when button is clicked", async () => {
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "1" });
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
const getActivitySpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"getViewableActivity"
|
||||
);
|
||||
const postStateChangeSpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"postActivityStateChange"
|
||||
);
|
||||
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
|
||||
const activityWithoutPayouts = { ...activity(), activityPayouts: [] };
|
||||
getActivitySpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
activity: activityWithoutPayouts,
|
||||
authorized: {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: [ActivityState.Rejected]
|
||||
},
|
||||
transferAction: mockActivityTransferAction(),
|
||||
stateChanges: changes()
|
||||
}
|
||||
});
|
||||
|
||||
postStateChangeSpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: activityWithoutPayouts
|
||||
});
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<ActivityDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("button[data-test='Rejected-activity-button']")
|
||||
.simulate("click");
|
||||
});
|
||||
|
||||
expect(postStateChangeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postStateChangeSpy).toHaveBeenCalledWith({
|
||||
activityId: 1,
|
||||
note: pageTranslate.stateChangeNote,
|
||||
state: "Rejected"
|
||||
});
|
||||
wrapper.update();
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should not call fetch activity again if post fails", async () => {
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "1" });
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
const getActivitySpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"getViewableActivity"
|
||||
);
|
||||
const postStateChangeSpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"postActivityStateChange"
|
||||
);
|
||||
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
|
||||
const activityWithoutPayouts = { ...activity(), activityPayouts: [] };
|
||||
getActivitySpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
activity: activityWithoutPayouts,
|
||||
authorized: {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: [ActivityState.Rejected]
|
||||
},
|
||||
transferAction: mockActivityTransferAction(),
|
||||
stateChanges: changes()
|
||||
}
|
||||
});
|
||||
|
||||
// return failure when posting
|
||||
postStateChangeSpy.mockResolvedValue({
|
||||
state: AsyncState.Failure
|
||||
});
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<ActivityDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("button[data-test='Rejected-activity-button']")
|
||||
.simulate("click");
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should post the activity transfer request", async () => {
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "1" });
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
const getActivitySpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"getViewableActivity"
|
||||
);
|
||||
const postRequestTransferSpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"postRequestTransfer"
|
||||
);
|
||||
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
|
||||
const activityWithoutPayouts = { ...activity(), activityPayouts: [] };
|
||||
getActivitySpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
activity: activityWithoutPayouts,
|
||||
authorized: {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
},
|
||||
transferAction: mockActivityTransferAction(),
|
||||
stateChanges: changes()
|
||||
}
|
||||
});
|
||||
|
||||
const returnedAction: IActivityTransferAction = {
|
||||
id: 1,
|
||||
activityStateBeforeTransfer: ActivityState.Accepted,
|
||||
requestedActivityId: 2,
|
||||
state: ActivityTransferState.Requested,
|
||||
newActivityDepartmentId: 3,
|
||||
newActivityEmployeeId: "C000"
|
||||
};
|
||||
postRequestTransferSpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: returnedAction
|
||||
});
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<ActivityDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
// select department and appUser
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("select[data-test='appUser-search-select']")
|
||||
.simulate("change", { target: { value: "C0002" } });
|
||||
wrapper
|
||||
.find("select[data-test='dept-input']")
|
||||
.simulate("change", { target: { value: "25" } });
|
||||
});
|
||||
|
||||
// submit transfer request
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("button[data-test='activity-transfer-request-button']")
|
||||
.simulate("click");
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(postRequestTransferSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postRequestTransferSpy).toHaveBeenCalledWith({
|
||||
activityStateBeforeTransfer: "Accepted",
|
||||
newDepartmentId: 25,
|
||||
newEmployeeId: "C0002",
|
||||
requestedActivityId: 1
|
||||
});
|
||||
wrapper.update();
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should post transfer approval request", async () => {
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({
|
||||
id: "1"
|
||||
});
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
const getActivitySpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"getViewableActivity"
|
||||
);
|
||||
const postApproveTransferSpy = jest.spyOn(
|
||||
ActivityService,
|
||||
"postApproveTransfer"
|
||||
);
|
||||
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
|
||||
const pendingTransferActivity = activity();
|
||||
pendingTransferActivity.state = ActivityState.PendingTransfer;
|
||||
getActivitySpy.mockResolvedValue({
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
activity: activity(),
|
||||
authorized: {
|
||||
canApproveTransferTo: true,
|
||||
canApproveTransferFrom: true,
|
||||
allowedChanges: []
|
||||
},
|
||||
transferAction: mockActivityTransferAction(),
|
||||
stateChanges: changes()
|
||||
}
|
||||
});
|
||||
|
||||
let wrapper: any;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<ActivityDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
expect(getActivitySpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='activity-approval-component']").length
|
||||
).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("button[data-test='reject-transfer-button']")
|
||||
.simulate("click");
|
||||
});
|
||||
|
||||
expect(postApproveTransferSpy).toHaveBeenCalledTimes(1);
|
||||
expect(postApproveTransferSpy).toHaveBeenCalledWith({
|
||||
id: 11,
|
||||
approvalState: ActivityTransferState.Rejected
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const activity = (): 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: [
|
||||
{
|
||||
id: 1,
|
||||
metaData: "test data",
|
||||
activityRef: 1,
|
||||
metaDataType: "Unknown"
|
||||
}
|
||||
],
|
||||
activityPayouts: [
|
||||
{
|
||||
payoutLineItem: {
|
||||
id: 1,
|
||||
activityPeriod: moment.utc(),
|
||||
departmentId: 25,
|
||||
role: {
|
||||
description: "Teller",
|
||||
name: "Teller Doe",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
product: "ATMCard",
|
||||
goalId: 2,
|
||||
goalValue: 3,
|
||||
commissionId: 4,
|
||||
commissionValue: 5,
|
||||
employeeId: "123",
|
||||
activityTotal: 100,
|
||||
payoutTotal: 200,
|
||||
slidingPayout: 100
|
||||
}
|
||||
},
|
||||
{
|
||||
payoutLineItem: {
|
||||
id: 2,
|
||||
activityPeriod: moment.utc(),
|
||||
departmentId: 25,
|
||||
role: {
|
||||
description: "Branch Manager",
|
||||
name: "B.M. Doe",
|
||||
permissions: [],
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
product: "ATMCard",
|
||||
goalId: 2,
|
||||
goalValue: 3,
|
||||
commissionId: 4,
|
||||
commissionValue: 5,
|
||||
employeeId: "456",
|
||||
activityTotal: 100,
|
||||
payoutTotal: 200,
|
||||
slidingPayout: 100
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
const changes = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
state: "Created",
|
||||
note: "Auto created",
|
||||
created: moment.utc()
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
state: "Created",
|
||||
note: "Auto created",
|
||||
created: moment.utc()
|
||||
}
|
||||
] as IActivityStateChange[];
|
||||
};
|
||||
|
||||
const mockDepartmentState: IDepartmentState = {
|
||||
departments: {
|
||||
data: [
|
||||
{
|
||||
departmentId: 25,
|
||||
id: 25,
|
||||
departmentName: "WAUWATOSA",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active",
|
||||
dnaOrgNbr: "111",
|
||||
regionCode: "2",
|
||||
dayForceGlCode: "33"
|
||||
}
|
||||
],
|
||||
state: AsyncState.Success
|
||||
},
|
||||
submitState: { state: AsyncState.Inactive }
|
||||
};
|
||||
|
||||
const mockProfileState: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
adEmail: "[email protected]",
|
||||
baseRole: "Test Role",
|
||||
departments: [
|
||||
{
|
||||
employeeId: "",
|
||||
departmentId: 25,
|
||||
startDate: moment(),
|
||||
note: "",
|
||||
isHome: true,
|
||||
slidingPayout: 100,
|
||||
role: {
|
||||
name: "Manager",
|
||||
description: "Manager",
|
||||
hasCommissions: true,
|
||||
permissions: [
|
||||
{
|
||||
code:
|
||||
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS,
|
||||
description: ""
|
||||
},
|
||||
{
|
||||
code:
|
||||
PermissionCode.SET_ACTIVITY_TO_MANUAL_REVIEW,
|
||||
description: ""
|
||||
}
|
||||
],
|
||||
rank: 99
|
||||
},
|
||||
created: moment(),
|
||||
createdBy: "",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedBy: "",
|
||||
lastUpdatedByType: UserType.Associate
|
||||
}
|
||||
],
|
||||
employeeId: "12345",
|
||||
fullName: "John Tester",
|
||||
homeDepartment: "",
|
||||
personId: 0,
|
||||
role: {
|
||||
name: "myrole",
|
||||
permissions: [
|
||||
{
|
||||
code:
|
||||
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS,
|
||||
description: ""
|
||||
}
|
||||
],
|
||||
description: "My Role",
|
||||
hasCommissions: true,
|
||||
rank: 99
|
||||
},
|
||||
additionalPermissions: [],
|
||||
assignments: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Inactive
|
||||
}
|
||||
};
|
||||
|
||||
const mockActivityTransferAction = (): IActivityTransferAction => {
|
||||
return {
|
||||
id: 11,
|
||||
activityStateBeforeTransfer: ActivityState.Accepted,
|
||||
newActivityDepartmentId: 25,
|
||||
newActivityEmployeeId: "C0002",
|
||||
requestedActivityId: 1,
|
||||
state: ActivityTransferState.Requested
|
||||
};
|
||||
};
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Col, Row } from "reactstrap";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import {
|
||||
ActivityState,
|
||||
ActivityTransferState,
|
||||
IActivityStateChangeDto,
|
||||
IActivityTransferAction,
|
||||
IActivityTransferApprovalRequestDto,
|
||||
IActivityTransferRequestDto,
|
||||
IAuditableActivity,
|
||||
IViewableActivityDetail
|
||||
} from "../../shared/models/Activity";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import ActivityDetailsAlertBar from "../../components/activity-detail/ActivityDetailAlertBar";
|
||||
import ActivityDisplay from "../../components/activity-detail/ActivityDisplay";
|
||||
import ActivityDetailDisplay from "../../components/activity-detail/ActivityDetailDisplay";
|
||||
import ActivityStateChangesTable from "../../components/activity-detail/ActivityStateChangesTable";
|
||||
import ActivityMetaDataDisplay from "../../components/activity-detail/ActivityMetaDataDisplay";
|
||||
import ActivityPayoutsDisplay from "../../components/activity-detail/ActivityPayoutsDisplay";
|
||||
import ActivityTransferRequest from "../../components/activity-detail/ActivityTransferRequest";
|
||||
import ActivityTransferApproval from "../../components/activity-detail/ActivityTransferApproval";
|
||||
import { useSelector } from "react-redux";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import ActivityTransferToOwnDepartmentRequest from "../../components/activity-detail/ActivityTransferToOwnDepartmentRequest";
|
||||
import { GetCurrentActiveDepartments } from "../../shared/models/Employee";
|
||||
import ActivityDetailUpdatesTable from "../../components/activity-detail/ActivityDetailUpdatesTable";
|
||||
import { Moment } from "moment";
|
||||
|
||||
const pageTranslate = Translate.Get.containers.activityDetailPage;
|
||||
|
||||
const ActivityDetailPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const [activity, setActivity] = useState<
|
||||
IAsyncStateContainer<IViewableActivityDetail>
|
||||
>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
|
||||
const [activityStateChange, setActivityStateChange] = useState<
|
||||
IAsyncStateContainer<IAuditableActivity>
|
||||
>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
|
||||
const [activityTransferChange, setActivityTransferChange] = useState<
|
||||
IAsyncStateContainer<IActivityTransferAction>
|
||||
>({ state: AsyncState.Inactive });
|
||||
|
||||
const profileState = useSelector<IAppState, IProfileState>(
|
||||
(state) => state.profileState
|
||||
);
|
||||
const myProfile = profileState.profile;
|
||||
const departments = settings?.departments.filter(
|
||||
(x) => x.state == "Active"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadActivity();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (activityStateChange.state === AsyncState.Success) {
|
||||
loadActivity();
|
||||
}
|
||||
}, [activityStateChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activityTransferChange.state === AsyncState.Success) {
|
||||
loadActivity();
|
||||
}
|
||||
}, [activityTransferChange]);
|
||||
|
||||
const loadActivity = async () => {
|
||||
setActivity({ state: AsyncState.Loading });
|
||||
const activity = await ActivityService.getViewableActivity(+id);
|
||||
setActivity(activity);
|
||||
};
|
||||
|
||||
const changeActivityState = async (
|
||||
newState: ActivityState,
|
||||
newDate: Moment | undefined
|
||||
) => {
|
||||
if (activity.data?.activity) {
|
||||
setActivityStateChange({ state: AsyncState.Loading });
|
||||
const change: IActivityStateChangeDto = {
|
||||
activityId: activity.data.activity.id,
|
||||
note: pageTranslate.stateChangeNote,
|
||||
acceptedDate: newDate,
|
||||
state: newState
|
||||
};
|
||||
const changedActivity = await ActivityService.postActivityStateChange(
|
||||
change
|
||||
);
|
||||
setActivityStateChange(changedActivity);
|
||||
}
|
||||
};
|
||||
|
||||
const submitTransferRequest = async (
|
||||
departmentId: string,
|
||||
employeeId: string
|
||||
) => {
|
||||
if (activity.data && activity.data.activity) {
|
||||
setActivityTransferChange({ state: AsyncState.Loading });
|
||||
const transferRequest: IActivityTransferRequestDto = {
|
||||
activityStateBeforeTransfer: activity.data.activity.state,
|
||||
requestedActivityId: activity.data.activity.id,
|
||||
newDepartmentId: +departmentId,
|
||||
newEmployeeId: employeeId
|
||||
};
|
||||
const requestResult = await ActivityService.postRequestTransfer(
|
||||
transferRequest
|
||||
);
|
||||
setActivityTransferChange(requestResult);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const submitTransferToOwnDepartmentRequest = async (
|
||||
departmentId: string,
|
||||
employeeId: string
|
||||
) => {
|
||||
if (activity.data && activity.data.activity) {
|
||||
setActivityTransferChange({ state: AsyncState.Loading });
|
||||
const transferRequest: IActivityTransferRequestDto = {
|
||||
activityStateBeforeTransfer: activity.data.activity.state,
|
||||
requestedActivityId: activity.data.activity.id,
|
||||
newDepartmentId: +departmentId,
|
||||
newEmployeeId: employeeId
|
||||
};
|
||||
const requestResult = await ActivityService.postRequestTransferToOwnDepartment(
|
||||
transferRequest
|
||||
);
|
||||
setActivityTransferChange(requestResult);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
const approveTransferRequest = async (
|
||||
requestId: number,
|
||||
decision: ActivityTransferState
|
||||
) => {
|
||||
setActivityTransferChange({ state: AsyncState.Loading });
|
||||
const transferApprovalRequest: IActivityTransferApprovalRequestDto = {
|
||||
id: requestId,
|
||||
approvalState: decision
|
||||
};
|
||||
const requestResult = await ActivityService.postApproveTransfer(
|
||||
transferApprovalRequest
|
||||
);
|
||||
setActivityTransferChange(requestResult);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActivityDetailsAlertBar
|
||||
activity={activity}
|
||||
activityStateChange={activityStateChange}
|
||||
activityTransferChange={activityTransferChange}
|
||||
/>
|
||||
<h3>{pageTranslate.title}</h3>
|
||||
{activity.data &&
|
||||
activity.data.activity &&
|
||||
activity.state === AsyncState.Success &&
|
||||
myProfile.data &&
|
||||
departments && (
|
||||
<div>
|
||||
<Row data-test="activity-details-components">
|
||||
<Col md="12" lg="6">
|
||||
<Row>
|
||||
<Col md="12" lg="6">
|
||||
<ActivityDisplay
|
||||
activity={activity.data.activity}
|
||||
onSetActivityState={(
|
||||
newState,
|
||||
newDate
|
||||
) =>
|
||||
changeActivityState(
|
||||
newState,
|
||||
newDate
|
||||
)
|
||||
}
|
||||
profile={myProfile.data}
|
||||
authorization={
|
||||
activity.data.authorized
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md="12" lg="6">
|
||||
<ActivityDetailDisplay
|
||||
detail={
|
||||
activity.data.activity.detail
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col md="12" lg="6">
|
||||
<Col lg="12">
|
||||
<ActivityPayoutsDisplay
|
||||
payouts={activity.data.activity.activityPayouts?.map(
|
||||
(activityPayouts) =>
|
||||
activityPayouts.payoutLineItem
|
||||
)}
|
||||
/>
|
||||
<ActivityStateChangesTable
|
||||
activityStateChanges={
|
||||
activity.data.stateChanges || []
|
||||
}
|
||||
/>
|
||||
<ActivityDetailUpdatesTable
|
||||
activityDetailUpdates={
|
||||
activity.data.detailUpdates || []
|
||||
}
|
||||
/>
|
||||
<ActivityMetaDataDisplay
|
||||
activityMetaData={activity.data.activity.metaData?.map(
|
||||
(data) => data
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md="12" lg="6">
|
||||
<Col lg="12">
|
||||
<ActivityTransferRequest
|
||||
activity={activity.data.activity}
|
||||
departments={departments}
|
||||
onFormSubmit={submitTransferRequest}
|
||||
/>
|
||||
<ActivityTransferApproval
|
||||
transferAction={
|
||||
activity.data.transferAction
|
||||
}
|
||||
activity={activity.data.activity}
|
||||
authorization={activity.data.authorized}
|
||||
approveTransfer={approveTransferRequest}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
<Col md="12" lg="6">
|
||||
<Col lg="12">
|
||||
<ActivityTransferToOwnDepartmentRequest
|
||||
activity={activity.data.activity}
|
||||
allDepartments={departments}
|
||||
ownedDepartments={GetCurrentActiveDepartments(
|
||||
myProfile.data.departments
|
||||
)}
|
||||
currentEmployeeId={
|
||||
myProfile.data.employeeId
|
||||
}
|
||||
onFormSubmit={
|
||||
submitTransferToOwnDepartmentRequest
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityDetailPage;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
import { ManualActivityWizard } from "../../components/activity/manual-activity-wizard/ManualActivityWizard";
|
||||
import {
|
||||
generateDefaultActivityForm,
|
||||
IManualActivityFormState
|
||||
} from "../../shared/models/Activity";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import { IDepartment } from "../../shared/models/Department";
|
||||
|
||||
const ActivityFormPage: React.FC = () => {
|
||||
const [activityForm, setActivityForm] = useState<IManualActivityFormState>(
|
||||
generateDefaultActivityForm()
|
||||
);
|
||||
// This is an experimental approach to showing loading state locally without breaking the pattern alex set.
|
||||
const [submitState, setSubmitState] = useState<AsyncState>(
|
||||
AsyncState.Inactive
|
||||
);
|
||||
|
||||
const settings = React.useContext(SettingsContext);
|
||||
|
||||
const submitActivity = async () => {
|
||||
setSubmitState(AsyncState.Loading);
|
||||
await ActivityService.submitManualActivityForm(activityForm);
|
||||
setSubmitState(AsyncState.Success);
|
||||
setActivityForm(generateDefaultActivityForm());
|
||||
};
|
||||
|
||||
let departments: IDepartment[] = [];
|
||||
if (settings?.departments) {
|
||||
departments = settings.departments.filter((x) => x.state == "Active");
|
||||
}
|
||||
|
||||
return (
|
||||
<ManualActivityWizard
|
||||
formState={activityForm}
|
||||
submitStatus={submitState}
|
||||
handleChange={(newState: IManualActivityFormState) => {
|
||||
setActivityForm(newState);
|
||||
}}
|
||||
submitManualActivity={submitActivity}
|
||||
clearForm={() => {
|
||||
setActivityForm(generateDefaultActivityForm());
|
||||
}}
|
||||
departments={departments}
|
||||
products={settings?.productDetails ?? []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityFormPage;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { shallow } from "enzyme";
|
||||
import * as React from "react";
|
||||
import * as reactRedux from "react-redux";
|
||||
import ActivityPage from "./ActivityPage";
|
||||
import { IActivityState } from "../../reducers/ActivityReducer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import ActivityPageFilterForm from "./ActivityPageFilterForm";
|
||||
import ActivityAuditLog from "../../components/activity/activity-audit-log/ActivityAuditLog";
|
||||
|
||||
describe("A component for finding activities", () => {
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useEffectMock = jest.spyOn(React, "useEffect");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
useEffectMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a blank search form", () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
const mockActivities: IActivityState = {
|
||||
activityStateFilter: [],
|
||||
currentUserDepartmentProgress: {
|
||||
state: AsyncState.Inactive
|
||||
},
|
||||
currentUserProgress: {
|
||||
state: AsyncState.Inactive
|
||||
},
|
||||
filteredActivitiesPageCache: [],
|
||||
filteredActivities: {
|
||||
data: {
|
||||
items: [],
|
||||
totalPages: 1,
|
||||
totalItems: 0,
|
||||
currentPage: 1
|
||||
},
|
||||
state: AsyncState.Success
|
||||
}
|
||||
};
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(mockActivities);
|
||||
const wrapper = shallow(<ActivityPage />);
|
||||
|
||||
const found = wrapper.find(ActivityPageFilterForm);
|
||||
expect(found.length).toBe(1);
|
||||
|
||||
const found2 = wrapper.find(ActivityAuditLog);
|
||||
expect(found2.length).toBe(1);
|
||||
|
||||
expect(dummyDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from "react";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import {
|
||||
getAllActivities,
|
||||
getLastPageOfActivities,
|
||||
getNextPageOfActivities
|
||||
} from "../../actions/ActivityActions";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { IActivityState } from "../../reducers/ActivityReducer";
|
||||
import ActivityAuditLog from "../../components/activity/activity-audit-log/ActivityAuditLog";
|
||||
import Configuration from "./../../shared/configuration/Configuration";
|
||||
import ActivityPageFilterForm from "./ActivityPageFilterForm";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { debounce } from "debounce";
|
||||
import { IActivitySearchForm } from "../../shared/models/Activity";
|
||||
|
||||
const ActivityAuditLogTranslate =
|
||||
Translate.Get.components.activity.activityAuditLog;
|
||||
|
||||
const ActivityPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [form, setForm] = React.useState<IActivitySearchForm>({
|
||||
state: [],
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
query: "",
|
||||
product: "",
|
||||
type: ""
|
||||
});
|
||||
|
||||
const activityState = useSelector<IAppState, IActivityState>(
|
||||
(state) => state.activityState
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
search(form);
|
||||
}, [form]);
|
||||
|
||||
const search = React.useCallback(
|
||||
debounce((query: IActivitySearchForm) => {
|
||||
dispatch(
|
||||
getAllActivities(
|
||||
1,
|
||||
Configuration.Get.defaultPaginationLimit,
|
||||
query
|
||||
)
|
||||
);
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
const changeSort = (headerName: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
sortBy: headerName,
|
||||
sortDescending:
|
||||
headerName === form.sortBy ? !form.sortDescending : false
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>{ActivityAuditLogTranslate.auditTableTitle}</h5>
|
||||
<ActivityPageFilterForm filterResults={setForm} />
|
||||
<ActivityAuditLog
|
||||
activityContainer={activityState.filteredActivities}
|
||||
getNextPage={() => dispatch(getNextPageOfActivities(form))}
|
||||
getPrevPage={() => dispatch(getLastPageOfActivities(form))}
|
||||
changeSort={changeSort}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityPage;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import ActivityPageFilterForm from "./ActivityPageFilterForm";
|
||||
import { IActivitySearchForm } from "../../shared/models/Activity";
|
||||
import { FormGroup } from "reactstrap";
|
||||
|
||||
describe("ActivePageFilterForm", () => {
|
||||
it("should display form correctly", () => {
|
||||
let testCalls = 0;
|
||||
const testCall = (filter: IActivitySearchForm) => {
|
||||
testCalls++;
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ActivityPageFilterForm filterResults={testCall} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(FormGroup).length).toEqual(8);
|
||||
expect(testCalls).toEqual(1);
|
||||
});
|
||||
|
||||
it("should respond to changes correctly", () => {
|
||||
let testCalls = 0;
|
||||
let lastFilter: IActivitySearchForm = {
|
||||
query: "",
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
state: [],
|
||||
product: "",
|
||||
type: "",
|
||||
employeeId: "",
|
||||
departmentId: 0
|
||||
};
|
||||
const testCall = (filter: IActivitySearchForm) => {
|
||||
lastFilter = filter;
|
||||
testCalls++;
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ActivityPageFilterForm filterResults={testCall} />
|
||||
);
|
||||
expect(wrapper.find(FormGroup).length).toEqual(8);
|
||||
|
||||
wrapper
|
||||
.find("[name='query']")
|
||||
.first()
|
||||
.simulate("change", { target: { value: "3" } });
|
||||
expect(testCalls).toEqual(2);
|
||||
expect(lastFilter.query).toEqual("3");
|
||||
|
||||
wrapper
|
||||
.find("[name='ActivityProduct']")
|
||||
.first()
|
||||
.simulate("change", { target: { value: "Vehicle" } });
|
||||
expect(testCalls).toEqual(3);
|
||||
expect(lastFilter.product).toEqual("Vehicle");
|
||||
expect(lastFilter.query).toEqual("3");
|
||||
|
||||
wrapper
|
||||
.find("[name='ActivityType']")
|
||||
.first()
|
||||
.simulate("change", { target: { value: "Sale" } });
|
||||
expect(testCalls).toEqual(4);
|
||||
expect(lastFilter.type).toEqual("Sale");
|
||||
expect(lastFilter.product).toEqual("Vehicle");
|
||||
expect(lastFilter.query).toEqual("3");
|
||||
});
|
||||
});
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormFeedback, FormGroup, Input, Label } from "reactstrap";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import { ActivityState, ActivityType } from "../../shared/models/Activity";
|
||||
import moment, { Moment } from "moment";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IActivitySearchForm } from "../../shared/models/Activity";
|
||||
import UserSearchSelect from "../../components/activity/manual-activity-wizard/UserSearchSelect";
|
||||
import Select from "react-select";
|
||||
|
||||
interface IActivityPageFilterFormProps {
|
||||
filterResults: (filter: IActivitySearchForm) => void;
|
||||
}
|
||||
|
||||
const activityFilterFormTranslate =
|
||||
Translate.Get.components.activity.filterForm;
|
||||
|
||||
const ActivityPageFilterForm: React.FC<IActivityPageFilterFormProps> = ({
|
||||
filterResults
|
||||
}) => {
|
||||
const [form, setForm] = useState<IActivitySearchForm>({
|
||||
state: ["Accepted"],
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
query: "",
|
||||
product: "",
|
||||
type: "",
|
||||
departmentId: 0,
|
||||
employeeId: ""
|
||||
});
|
||||
|
||||
const settings = React.useContext(SettingsContext);
|
||||
useEffect(() => {
|
||||
filterResults(form);
|
||||
}, [form]);
|
||||
const departments = settings?.departments || [];
|
||||
|
||||
const updateProduct = (newProduct: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
product: newProduct
|
||||
});
|
||||
};
|
||||
|
||||
const updateType = (newType: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
type: newType
|
||||
});
|
||||
};
|
||||
|
||||
const updateState = (newState: string[]) => {
|
||||
setForm({
|
||||
...form,
|
||||
state: newState
|
||||
});
|
||||
};
|
||||
|
||||
const setSearchQuery = (newQuery: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
query: newQuery
|
||||
});
|
||||
};
|
||||
|
||||
const updateLowDate = (date: Moment) => {
|
||||
setForm({
|
||||
...form,
|
||||
dateLow: date
|
||||
});
|
||||
};
|
||||
|
||||
const updateHighDate = (date: Moment) => {
|
||||
setForm({
|
||||
...form,
|
||||
dateHigh: date
|
||||
});
|
||||
};
|
||||
|
||||
const updateDepartment = (departmentId: number) => {
|
||||
setForm({
|
||||
...form,
|
||||
departmentId: departmentId
|
||||
});
|
||||
};
|
||||
|
||||
const updateEmployee = (employeeId?: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
employeeId: employeeId
|
||||
});
|
||||
};
|
||||
|
||||
const activityStateOptions = Object.values(ActivityState).map(
|
||||
(state) => new Option(state, state)
|
||||
);
|
||||
|
||||
const acceptedActivityStateOption = activityStateOptions.find(
|
||||
(state) => state.value === "Accepted"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.productType.label}</Label>
|
||||
<Input
|
||||
name="ActivityProduct"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={form.product === "" ? "" : form.product}
|
||||
onChange={(event) => {
|
||||
updateProduct(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" key="default_prod_type_option">
|
||||
{activityFilterFormTranslate.productType.default}
|
||||
</option>
|
||||
{settings?.productDetails.map((product) => {
|
||||
return (
|
||||
<option
|
||||
key={product.product}
|
||||
value={product.product}
|
||||
>
|
||||
{product.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.activityType.label}</Label>
|
||||
<Input
|
||||
name="ActivityType"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={form.type === "" ? "" : form.type}
|
||||
onChange={(event) => {
|
||||
updateType(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" key="default_activity_state">
|
||||
{activityFilterFormTranslate.activityType.default}
|
||||
</option>
|
||||
{Object.values(ActivityType).map((activityType) => {
|
||||
return (
|
||||
<option key={activityType}>{activityType}</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.activityState.label}</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>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>
|
||||
{activityFilterFormTranslate.departmentId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="CreditToDepartment"
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="dept-input"
|
||||
value={form.departmentId}
|
||||
onChange={(event) => {
|
||||
updateDepartment(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
key={undefined}
|
||||
value=""
|
||||
>
|
||||
{activityFilterFormTranslate.departmentId.placeHolder}
|
||||
</option>
|
||||
{departments.map((department) => {
|
||||
return (
|
||||
<option key={department.id} value={department.id}>
|
||||
{department.departmentName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
<FormFeedback>Please select a value.</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-6 col-lg-4">
|
||||
<Label>
|
||||
{activityFilterFormTranslate.employeeId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<UserSearchSelect
|
||||
data-test="CreditToAssociate"
|
||||
employeeId={form.employeeId ?? ""}
|
||||
onUserSelected={(field, value) => {
|
||||
updateEmployee(value);
|
||||
}}
|
||||
showValidation={false}
|
||||
placeHolder={
|
||||
activityFilterFormTranslate.employeeId.placeHolder
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.lowDate.label}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
data-test="low-date-input"
|
||||
value={form.dateLow?.format("YYYY-MM-DD").toString() ?? ""}
|
||||
invalid={false}
|
||||
onChange={(event) => {
|
||||
updateLowDate(moment.utc(event.target.value));
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
DeptAssignmentTranslate.validation.endDate
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.highDate.label}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
data-test="high-date-input"
|
||||
value={form.dateHigh?.format("YYYY-MM-DD").toString() ?? ""}
|
||||
invalid={false}
|
||||
onChange={(event) => {
|
||||
updateHighDate(moment.utc(event.target.value));
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
DeptAssignmentTranslate.validation.endDate
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.searchQuery.label}</Label>
|
||||
<Input
|
||||
name="query"
|
||||
bsSize="sm"
|
||||
value={form.query}
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.target.value);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityFilterFormTranslate.searchQuery.placeHolder
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityPageFilterForm;
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import CommissionRatesEditor from "./CommissionRatesEditor";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IAppRole, ISettings } from "../../shared/models/Settings";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import moment from "moment";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import { IProductDetail } from "../../shared/enumerations/Product";
|
||||
import CommissionService from "../../shared/services/CommissionService";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { getMockAppSettings } from "../../shared/testing/mocks/MockAppSettings";
|
||||
|
||||
describe("A container for setting commission rates", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
useContextMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a table with a row and cell for every product and role with commissions", () => {
|
||||
const mockSelectorValue = defaultMockSelectorValue();
|
||||
|
||||
const settings = getMockAppSettings();
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(mockSelectorValue);
|
||||
useContextMock.mockReturnValue(settings);
|
||||
|
||||
const editor = mount(<CommissionRatesEditor />);
|
||||
|
||||
const rolesWithCommissions =
|
||||
settings?.appRoles.filter((role) => role.hasCommissions) ?? [];
|
||||
|
||||
const productsWithCommissions =
|
||||
settings?.productDetails.filter(
|
||||
(product) => product.hasCommissions
|
||||
) ?? [];
|
||||
|
||||
const totalCells =
|
||||
productsWithCommissions.length * rolesWithCommissions.length;
|
||||
|
||||
expect(editor.find("[data-test='commission-rates-table']").length).toBe(
|
||||
1
|
||||
);
|
||||
expect(editor.find("[data-test='commission-rate-cell']").length).toBe(
|
||||
totalCells
|
||||
);
|
||||
expect(editor.find("[data-test='commission-rate-row']").length).toBe(
|
||||
productsWithCommissions.length
|
||||
);
|
||||
});
|
||||
|
||||
it("saves when there are pending changes", async () => {
|
||||
const mockSelectorValue = defaultMockSelectorValue();
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
useSelectorMock.mockReturnValue(mockSelectorValue);
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
const setCommissionsMock = jest
|
||||
.spyOn(CommissionService, "setCommissions")
|
||||
.mockResolvedValue({ state: AsyncState.Success });
|
||||
|
||||
const editor = mount(<CommissionRatesEditor />);
|
||||
|
||||
const saveButton = () =>
|
||||
editor.find("[data-test='commission-save-btn']");
|
||||
expect(saveButton().props().disabled).toBe(true);
|
||||
|
||||
// make 2 pending commission rate changes
|
||||
const cells = editor.find("[data-test='commission-rate-cell']");
|
||||
cells
|
||||
.at(0)
|
||||
.find("input")
|
||||
.simulate("change", { target: { value: "3" } });
|
||||
cells
|
||||
.at(1)
|
||||
.find("input")
|
||||
.simulate("change", { target: { value: "4" } });
|
||||
|
||||
// verify save button
|
||||
expect(saveButton().props().disabled).toBe(false);
|
||||
await act(async () => {
|
||||
saveButton().simulate("click");
|
||||
});
|
||||
editor.update();
|
||||
|
||||
expect(setCommissionsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setCommissionsMock).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 0,
|
||||
product: "ATMCard",
|
||||
ratePerProduct: 3,
|
||||
role: {
|
||||
name: "Mortgage",
|
||||
description: "Mortgage",
|
||||
permissions: [
|
||||
{
|
||||
code: "ELIGIBLE_FOR_REFERRALS",
|
||||
description: "User is eligible for referrals"
|
||||
},
|
||||
{
|
||||
code: "VIEW_DASHBOARD",
|
||||
description:
|
||||
"User can see a dashboard showing their referral totals and their sales totals compared to goal"
|
||||
}
|
||||
],
|
||||
hasCommissions: true,
|
||||
rank: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 0,
|
||||
product: "ATMCard",
|
||||
ratePerProduct: 4,
|
||||
role: {
|
||||
name: "Manager",
|
||||
description: "Manager",
|
||||
permissions: [
|
||||
{
|
||||
code: "VIEW_PERFORMANCE_FOR_DEPARTMENT",
|
||||
description:
|
||||
"User can see performance for their department"
|
||||
},
|
||||
{
|
||||
code: "MANAGE_DEPARTMENT_GOALS",
|
||||
description:
|
||||
"User can manange their department goals"
|
||||
},
|
||||
{
|
||||
code: "ASSIGN_ASSOCIATES_TO_DEPARTMENT",
|
||||
description:
|
||||
"User can assign associates to additional departments"
|
||||
},
|
||||
{
|
||||
code: "TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS",
|
||||
description:
|
||||
"User can transfer activities between deparments"
|
||||
},
|
||||
{
|
||||
code: "TRANSFER_ACTIVITIES_BETWEEN_ASSOCIATES",
|
||||
description:
|
||||
"User can transfer activities between associates"
|
||||
}
|
||||
],
|
||||
hasCommissions: true,
|
||||
rank: 1
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const saveSpan = editor.find("[data-test='commission-save-span']");
|
||||
expect(saveSpan.text()).toContain("Changes Saved");
|
||||
expect(saveButton().props().disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("Displays error if success not returned from commission service", async () => {
|
||||
const mockSelectorValue = defaultMockSelectorValue();
|
||||
useDispatchMock.mockImplementation(() => jest.fn());
|
||||
useSelectorMock.mockReturnValue(mockSelectorValue);
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
jest.spyOn(CommissionService, "setCommissions").mockResolvedValue({
|
||||
state: AsyncState.Failure,
|
||||
data: "There was an error saving commissions"
|
||||
});
|
||||
|
||||
const editor = mount(<CommissionRatesEditor />);
|
||||
|
||||
const saveButton = () =>
|
||||
editor.find("[data-test='commission-save-btn']");
|
||||
expect(saveButton().props().disabled).toBe(true);
|
||||
|
||||
// make a pending commission rate changes
|
||||
const cells = editor.find("[data-test='commission-rate-cell']");
|
||||
cells
|
||||
.at(0)
|
||||
.find("input")
|
||||
.simulate("change", { target: { value: "3" } });
|
||||
|
||||
// save
|
||||
await act(async () => {
|
||||
saveButton().simulate("click");
|
||||
});
|
||||
editor.update();
|
||||
|
||||
// verify failure message
|
||||
const saveSpan = editor.find("[data-test='commission-save-span']");
|
||||
expect(saveSpan.text()).toEqual(
|
||||
"There was an error saving commissions"
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays please wait... if no commissions returned from state", () => {
|
||||
const mockSelectorValue = {
|
||||
appSettings: {
|
||||
data: {
|
||||
appRoles: [] as IAppRole[],
|
||||
productDetails: [] as IProductDetail[]
|
||||
} as ISettings
|
||||
},
|
||||
commissions: {
|
||||
state: AsyncState.Inactive
|
||||
},
|
||||
submitState: {
|
||||
state: AsyncState.Inactive
|
||||
}
|
||||
};
|
||||
|
||||
useDispatchMock.mockReturnValue(jest.fn());
|
||||
useSelectorMock.mockReturnValue(mockSelectorValue);
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
|
||||
const editor = mount(<CommissionRatesEditor />);
|
||||
|
||||
expect(editor.find("[data-test='commissions-wait']").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
function defaultMockSelectorValue() {
|
||||
return {
|
||||
commissions: {
|
||||
data: [
|
||||
{
|
||||
product: "Commissions 1",
|
||||
role: {
|
||||
name: "BOM",
|
||||
description: "Branch Operations Manager",
|
||||
permissions: [{ code: "HAVE_SALES_GOALS" }],
|
||||
hasCommissions: true
|
||||
},
|
||||
id: 1,
|
||||
ratePerProduct: 11,
|
||||
created: moment(),
|
||||
createdBy: "",
|
||||
createdByType: UserType.Associate,
|
||||
lastUpdated: moment(),
|
||||
lastUpdatedBy: "",
|
||||
lastUpdatedByType: UserType.Associate
|
||||
}
|
||||
],
|
||||
state: AsyncState.Inactive
|
||||
},
|
||||
submitState: {
|
||||
state: AsyncState.Inactive
|
||||
}
|
||||
};
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import React from "react";
|
||||
import { ICommissionRateChange } from "../../shared/models/Commission";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import CommissionRatesTable from "../../components/commissions/CommissionRatesTable";
|
||||
import { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getAllCommissions } from "../../actions/CommissionActions";
|
||||
import { ICommissionState } from "../../reducers/CommissionReducer";
|
||||
import CommissionService from "../../shared/services/CommissionService";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import moment from "moment";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
|
||||
const CommissionRatesEditor: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const settings = React.useContext(SettingsContext);
|
||||
|
||||
const commissionsState = useSelector<IAppState, ICommissionState>(
|
||||
(state) => state.commissionState
|
||||
);
|
||||
|
||||
const commissions = CommissionService.mapToChange(
|
||||
commissionsState.commissions.data ?? []
|
||||
);
|
||||
|
||||
const [pendingChanges, setPendingChanges] = useState<
|
||||
ICommissionRateChange[]
|
||||
>([]);
|
||||
|
||||
const [saveMessage, setSaveMessage] = useState<string>("");
|
||||
const [submitState, setSubmitState] = useState<AsyncState>(
|
||||
AsyncState.Inactive
|
||||
);
|
||||
|
||||
const rolesWithCommissions =
|
||||
settings?.appRoles.filter((role) => role.hasCommissions) ?? [];
|
||||
|
||||
const productsWithCommissions =
|
||||
settings?.productDetails.filter((product) => product.hasCommissions) ??
|
||||
[];
|
||||
|
||||
const isValidChanges = (changes: ICommissionRateChange[]): boolean => {
|
||||
let isValid = true;
|
||||
changes.forEach((change) => {
|
||||
if (!CommissionService.isValidRateChange(change)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const changeRate = (change: ICommissionRateChange) => {
|
||||
setSubmitState(AsyncState.Inactive);
|
||||
const changes = [...pendingChanges];
|
||||
|
||||
const original = commissions.find(
|
||||
(com) =>
|
||||
com.role.name === change.role.name &&
|
||||
com.product === change.product
|
||||
);
|
||||
|
||||
const index = pendingChanges.findIndex(
|
||||
(com) =>
|
||||
com.role.name === change.role.name &&
|
||||
com.product === change.product
|
||||
);
|
||||
|
||||
if (index < 0) {
|
||||
changes.push(change); // add new
|
||||
} else if (original?.ratePerProduct === change.ratePerProduct) {
|
||||
changes.splice(index, 1); // remove change if it is the same as original value
|
||||
} else {
|
||||
changes[index] = { ...changes[index], ...change }; // update already pending change
|
||||
}
|
||||
|
||||
setPendingChanges(changes);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setSubmitState(AsyncState.Loading);
|
||||
|
||||
await CommissionService.setCommissions(pendingChanges).then(
|
||||
(responseState) => {
|
||||
setSubmitState(responseState.state);
|
||||
if (responseState.state === AsyncState.Success) {
|
||||
setSaveMessage("Changes Saved " + moment().format("LTS"));
|
||||
setPendingChanges([]);
|
||||
dispatch(getAllCommissions());
|
||||
} else {
|
||||
setSaveMessage(responseState.data ?? "");
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const canSave = () => {
|
||||
return (
|
||||
(submitState === AsyncState.Inactive ||
|
||||
submitState === AsyncState.Success) &&
|
||||
isValidChanges(pendingChanges) &&
|
||||
pendingChanges.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex justify-content-end align-items-center">
|
||||
<span className="mr-2" data-test="commission-save-span">
|
||||
{saveMessage}
|
||||
</span>
|
||||
<button
|
||||
data-test="commission-save-btn"
|
||||
className="btn btn-outline-success my-2"
|
||||
onClick={save}
|
||||
type="button"
|
||||
disabled={!canSave()}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{commissionsState.commissions.state &&
|
||||
commissionsState.commissions.data &&
|
||||
rolesWithCommissions.length &&
|
||||
productsWithCommissions.length ? (
|
||||
<CommissionRatesTable
|
||||
commissions={commissions}
|
||||
roles={rolesWithCommissions}
|
||||
products={productsWithCommissions}
|
||||
onRateChange={changeRate}
|
||||
/>
|
||||
) : (
|
||||
<div data-test="commissions-wait">Please Wait... </div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommissionRatesEditor;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getAllCommissions } from "../../actions/CommissionActions";
|
||||
import { ICommissionState } from "../../reducers/CommissionReducer";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { IAppRole } from "../../shared/models/Settings";
|
||||
import { Card, CardTitle, Table } from "reactstrap";
|
||||
import Translate from "../../translate/Translate";
|
||||
import ProductDisplay from "../shared/ProductDisplay";
|
||||
|
||||
export interface IPayoutProfileProps {
|
||||
role: IAppRole | undefined;
|
||||
}
|
||||
|
||||
const payoutTranslate = Translate.Get.components.payouts;
|
||||
|
||||
const CommissionsForRole: React.FC<IPayoutProfileProps> = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const commissionState = useSelector<IAppState, ICommissionState>(
|
||||
(state) => state.commissionState
|
||||
);
|
||||
let commissions = commissionState.commissions.data ?? [];
|
||||
|
||||
if (commissions) {
|
||||
commissions = commissions.filter(
|
||||
(commission) =>
|
||||
props.role?.name === commission.role.name &&
|
||||
commission.ratePerProduct > 0
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllCommissions());
|
||||
}, []);
|
||||
|
||||
if (
|
||||
!commissions ||
|
||||
commissions.length === 0 ||
|
||||
!props.role?.hasCommissions
|
||||
) {
|
||||
return <> No Commissions for {props.role?.name}.</>;
|
||||
}
|
||||
return (
|
||||
<Card body outline color="info">
|
||||
<CardTitle>
|
||||
<strong>Commissions for {props.role?.name}</strong>
|
||||
</CardTitle>
|
||||
<Table
|
||||
striped
|
||||
bordered
|
||||
size="sm"
|
||||
className=" tablecenterCSS tablecenterheadCSS"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{payoutTranslate.product}</th>
|
||||
<th>{payoutTranslate.pays}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commissions.map((commission) => {
|
||||
return (
|
||||
<tr key={commission.id}>
|
||||
<td>
|
||||
<ProductDisplay
|
||||
productCode={commission.product}
|
||||
/>
|
||||
</td>
|
||||
<td>${commission.ratePerProduct}.00</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommissionsForRole;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getAllCommissions } from "../../actions/CommissionActions";
|
||||
import { ICommissionState } from "../../reducers/CommissionReducer";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import TabbedCommissionView from "../../components/commissions/TabbedCommissionView";
|
||||
|
||||
const CommissionPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const commissionState = useSelector<IAppState, ICommissionState>(
|
||||
(state) => state.commissionState
|
||||
);
|
||||
|
||||
const commissions = commissionState.commissions.data ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllCommissions());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{Translate.Get.containers.commissionPage.title}</h2>
|
||||
<TabbedCommissionView commissions={commissions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommissionPage;
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IDashboardArgs } from "./DashboardPage";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { IBusinessLineProgress } from "../../shared/models/Activity";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import { Alert, Form, Input } from "reactstrap";
|
||||
import MultiDepartmentProgressDisplay from "../../components/department-progress/MultiDepartmentProgressDisplay";
|
||||
import BusinessLineSummaryTable from "./BusinessLineSummaryTable";
|
||||
|
||||
const BusinessLineDashboard: React.FC<IDashboardArgs> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const [blProgress, setBlProgress] = useState<
|
||||
IAsyncStateContainer<IBusinessLineProgress[]>
|
||||
>({ state: AsyncState.Inactive });
|
||||
const [selectedBl, setSelectedBl] = useState<string>("ALL");
|
||||
const range = props.range;
|
||||
const allBl = settings?.businessLines || [];
|
||||
|
||||
useEffect(() => {
|
||||
setBlProgress({ state: AsyncState.Loading });
|
||||
retrieveProgress();
|
||||
}, [
|
||||
range.toMonth,
|
||||
range.toYear,
|
||||
range.fromMonth,
|
||||
range.fromYear,
|
||||
selectedBl
|
||||
]);
|
||||
|
||||
const retrieveProgress = () => {
|
||||
if (range) {
|
||||
ActivityService.getBusinessProgress(range, selectedBl)
|
||||
.then((response) => {
|
||||
setBlProgress(response);
|
||||
})
|
||||
.catch(() => {
|
||||
setBlProgress({ state: AsyncState.Failure });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncGuard container={blProgress}>
|
||||
{blProgress.data ? (
|
||||
<>
|
||||
<Form inline noValidate>
|
||||
<Input
|
||||
inline
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="dept-input"
|
||||
value={selectedBl}
|
||||
onChange={(event) => {
|
||||
setSelectedBl(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option data-test="all-option" key="" value="ALL">
|
||||
LCU
|
||||
</option>
|
||||
{allBl.map((bl) => {
|
||||
return (
|
||||
<option key={bl.code} value={bl.code}>
|
||||
{bl.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</Form>
|
||||
{selectedBl === "ALL" ? (
|
||||
<BusinessLineSummaryTable
|
||||
blProgress={blProgress.data}
|
||||
/>
|
||||
) : (
|
||||
<MultiDepartmentProgressDisplay
|
||||
departments={blProgress.data[0].departments}
|
||||
monthRange={blProgress.data[0].monthRange}
|
||||
label={blProgress.data[0].businessLine.name}
|
||||
showDetails
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Alert color="warning">
|
||||
Unable to load your business information.
|
||||
</Alert>
|
||||
)}
|
||||
</AsyncGuard>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessLineDashboard;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import {
|
||||
IBusinessLineProgress,
|
||||
IProgressTotal
|
||||
} from "../../shared/models/Activity";
|
||||
import ProductTableHeader from "../../components/shared/ProductTableHeader";
|
||||
import ProgressTableRow from "../../components/shared/ProgressTableRow";
|
||||
import { IBusinessLine } from "../../shared/models/Settings";
|
||||
import ProgressPercentageTableRow from "../../components/shared/ProgressPercentageTableRow";
|
||||
|
||||
export interface IRegionSummaryArgs {
|
||||
blProgress: IBusinessLineProgress[];
|
||||
}
|
||||
|
||||
const BusinessLineSummaryTable: React.FC<IRegionSummaryArgs> = (props) => {
|
||||
const blTotals: IProgressTotal[] = [];
|
||||
const bls: IBusinessLine[] = [];
|
||||
props.blProgress.forEach((bl) => {
|
||||
const totals = ActivityService.calculateTotalProgress(bl.departments);
|
||||
blTotals.push(totals);
|
||||
bls.push(bl.businessLine);
|
||||
});
|
||||
|
||||
const allBusinessLineTotal = ActivityService.joinTotalProgress(blTotals);
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped tablecenterCSS tablecenterheadCSS"
|
||||
data-test="business-line-summary-table"
|
||||
>
|
||||
<ProductTableHeader />
|
||||
<tbody>
|
||||
{blTotals.map((eachTotal, idx) => (
|
||||
<ProgressTableRow
|
||||
label={bls[idx].name}
|
||||
progress={eachTotal.activityTotal}
|
||||
goals={eachTotal.goalTotal}
|
||||
key={idx}
|
||||
/>
|
||||
))}
|
||||
<ProgressPercentageTableRow
|
||||
label="Goal Progress"
|
||||
progress={allBusinessLineTotal.activityTotal}
|
||||
goals={allBusinessLineTotal.goalTotal}
|
||||
/>
|
||||
<ProgressTableRow
|
||||
label="Totals"
|
||||
progress={allBusinessLineTotal.activityTotal}
|
||||
goals={allBusinessLineTotal.goalTotal}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessLineSummaryTable;
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useState } from "react";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import ProfileService from "../../shared/services/ProfileService";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import DepartmentDashboard from "./DepartmentDashboard";
|
||||
import EmployeeDashboard from "./EmployeeDashboard";
|
||||
import EmptyDashboard from "./EmptyDashboard";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import RegionDashboard from "./RegionDashboard";
|
||||
import BusinessLineDashboard from "./BusinessLineDashboard";
|
||||
import GoalProgressControls from "../../components/goal/GoalProgressControls";
|
||||
import { IMonthRange } from "../../shared/types/IMonthRange";
|
||||
import moment from "moment";
|
||||
|
||||
export interface IDashboardArgs {
|
||||
range: IMonthRange;
|
||||
}
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const [range, setRange] = useState<IMonthRange>({
|
||||
fromMonth: moment().month() + 1,
|
||||
fromYear: moment().year(),
|
||||
toMonth: moment().month() + 1,
|
||||
toYear: moment().year()
|
||||
});
|
||||
const profileState = useSelector<IAppState, IProfileState>(
|
||||
(state) => state.profileState
|
||||
);
|
||||
const myProfile = profileState.profile;
|
||||
const search = useLocation().search;
|
||||
const view = new URLSearchParams(search).get("dashboard");
|
||||
|
||||
if (!myProfile || !myProfile.data) {
|
||||
return <EmptyDashboard />;
|
||||
}
|
||||
|
||||
const noParamFinder = () => {
|
||||
const permission = ProfileService.highestRole(
|
||||
myProfile.data as IEmployee
|
||||
);
|
||||
|
||||
switch (permission) {
|
||||
case PermissionCode.VIEW_PERFORMANCE_FOR_REGION:
|
||||
return <RegionDashboard range={range} />;
|
||||
case PermissionCode.VIEW_PERFORMANCE_FOR_BUSINESS_LINE:
|
||||
return <BusinessLineDashboard range={range} />;
|
||||
case PermissionCode.VIEW_PERFORMANCE_FOR_DEPARTMENT:
|
||||
return <DepartmentDashboard range={range} />;
|
||||
case PermissionCode.VIEW_DASHBOARD:
|
||||
return <EmployeeDashboard range={range} />;
|
||||
case PermissionCode.UNKNOWN:
|
||||
default:
|
||||
return <EmptyDashboard />;
|
||||
}
|
||||
};
|
||||
|
||||
const paramFinder = (param: string) => {
|
||||
switch (param) {
|
||||
case "region":
|
||||
return <RegionDashboard range={range} />;
|
||||
case "business":
|
||||
return <BusinessLineDashboard range={range} />;
|
||||
case "department":
|
||||
return <DepartmentDashboard range={range} />;
|
||||
case "employee":
|
||||
return <EmployeeDashboard range={range} />;
|
||||
case "empty":
|
||||
default:
|
||||
return <EmptyDashboard />;
|
||||
}
|
||||
};
|
||||
const onGoalProgressRangeChange = (range: IMonthRange) => {
|
||||
setRange(range);
|
||||
};
|
||||
|
||||
const selectedDashboard = view ? paramFinder(view) : noParamFinder();
|
||||
return (
|
||||
<div data-test="dashboard-chooser">
|
||||
<AsyncGuard container={myProfile}>
|
||||
<>
|
||||
<GoalProgressControls
|
||||
onChange={onGoalProgressRangeChange}
|
||||
/>
|
||||
{selectedDashboard}
|
||||
</>
|
||||
</AsyncGuard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IDepartmentProgress } from "../../shared/models/Activity";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import TabbedDepartmentProgressView from "../../components/department-progress/TabbedDepartmentProgressView";
|
||||
import { IDashboardArgs } from "./DashboardPage";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const DepartmentDashboard: React.FC<IDashboardArgs> = (props) => {
|
||||
const range = props.range;
|
||||
const [myProgress, setMyProgress] = useState<
|
||||
IAsyncStateContainer<IDepartmentProgress[]>
|
||||
>({ state: AsyncState.Loading });
|
||||
const search = useLocation().search;
|
||||
const departmentId =
|
||||
new URLSearchParams(search).get("departmentId") || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
setMyProgress({ state: AsyncState.Loading });
|
||||
retrieveProgress();
|
||||
}, [
|
||||
range.toMonth,
|
||||
range.toYear,
|
||||
range.fromMonth,
|
||||
range.fromYear,
|
||||
departmentId
|
||||
]);
|
||||
|
||||
const retrieveProgress = () => {
|
||||
if (range) {
|
||||
ActivityService.getMyDepartmentProgress(range, departmentId)
|
||||
.then((response) => {
|
||||
setMyProgress(response);
|
||||
})
|
||||
.catch(() => {
|
||||
setMyProgress({ state: AsyncState.Failure });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncGuard container={myProgress}>
|
||||
{myProgress.data && myProgress.data.length > 0 ? (
|
||||
<>
|
||||
{myProgress.data.map((progressData) => {
|
||||
return (
|
||||
<TabbedDepartmentProgressView
|
||||
departmentProgress={progressData}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</AsyncGuard>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentDashboard;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import TabbedProgressView from "../../components/goal/TabbedProgressView";
|
||||
import { IDashboardArgs } from "./DashboardPage";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { IEmployeeProgress } from "../../shared/models/Activity";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import UserDisplay from "../shared/UserDisplay";
|
||||
|
||||
const EmployeeDashboard: React.FC<IDashboardArgs> = (props) => {
|
||||
const range = props.range;
|
||||
const [myProgress, setMyProgress] = useState<
|
||||
IAsyncStateContainer<IEmployeeProgress>
|
||||
>({ state: AsyncState.Loading });
|
||||
const search = useLocation().search;
|
||||
const employeeId =
|
||||
new URLSearchParams(search).get("employeeId") || undefined;
|
||||
|
||||
useEffect(() => {
|
||||
setMyProgress({ state: AsyncState.Loading });
|
||||
retrieveProgress();
|
||||
}, [
|
||||
range.toMonth,
|
||||
range.toYear,
|
||||
range.fromMonth,
|
||||
range.fromYear,
|
||||
employeeId
|
||||
]);
|
||||
|
||||
const retrieveProgress = () => {
|
||||
if (range) {
|
||||
ActivityService.getMyProgress(range, employeeId)
|
||||
.then((response) => {
|
||||
setMyProgress(response);
|
||||
})
|
||||
.catch(() => {
|
||||
setMyProgress({ state: AsyncState.Failure });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncGuard container={myProgress}>
|
||||
<div>
|
||||
{employeeId ? (
|
||||
<h4>
|
||||
<UserDisplay employeeId={employeeId} />
|
||||
</h4>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<TabbedProgressView userProgress={myProgress.data!} />
|
||||
</div>
|
||||
</AsyncGuard>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeDashboard;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import EmptyDashboard from "./EmptyDashboard";
|
||||
|
||||
describe("A Component For displaying an empty dashboard", () => {
|
||||
it("Displays a basic component", () => {
|
||||
const wrapper = shallow(<EmptyDashboard />);
|
||||
expect(wrapper.find("[data-test='empty-dashboard']").length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
const EmptyDashboard: React.FC = () => {
|
||||
return (
|
||||
<div data-test="empty-dashboard" className="mt-2">
|
||||
<Alert color="danger">
|
||||
Sorry, you currently don’t have permission to access this
|
||||
application. Contact IT if you require access.
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyDashboard;
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import { IRegionProgress } from "../../shared/models/Activity";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import MultiDepartmentProgressDisplay from "../../components/department-progress/MultiDepartmentProgressDisplay";
|
||||
import { Alert, Form, Input } from "reactstrap";
|
||||
import { IDashboardArgs } from "./DashboardPage";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import RegionSummaryTable from "./RegionSummaryTable";
|
||||
|
||||
const RegionDashboard: React.FC<IDashboardArgs> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const [regionProgress, setRegionProgress] = useState<
|
||||
IAsyncStateContainer<IRegionProgress[]>
|
||||
>({ state: AsyncState.Inactive });
|
||||
const [selectedRegion, setSelectedRegion] = useState<string>("ALL");
|
||||
const range = props.range;
|
||||
const allRegions = settings?.regions || [];
|
||||
|
||||
useEffect(() => {
|
||||
setRegionProgress({ state: AsyncState.Loading });
|
||||
retrieveProgress();
|
||||
}, [
|
||||
range.toMonth,
|
||||
range.toYear,
|
||||
range.fromMonth,
|
||||
range.fromYear,
|
||||
selectedRegion
|
||||
]);
|
||||
|
||||
const retrieveProgress = () => {
|
||||
if (range) {
|
||||
ActivityService.getRegionProgress(range, selectedRegion)
|
||||
.then((response) => {
|
||||
setRegionProgress(response);
|
||||
})
|
||||
.catch(() => {
|
||||
setRegionProgress({ state: AsyncState.Failure });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncGuard container={regionProgress}>
|
||||
{regionProgress.data ? (
|
||||
<>
|
||||
<Form inline noValidate>
|
||||
<Input
|
||||
inline
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="dept-input"
|
||||
value={selectedRegion}
|
||||
onChange={(event) => {
|
||||
setSelectedRegion(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option data-test="all-option" key="" value="ALL">
|
||||
All Regions
|
||||
</option>
|
||||
{allRegions.map((region) => {
|
||||
return (
|
||||
<option
|
||||
key={region.code}
|
||||
value={region.code}
|
||||
>
|
||||
{region.name} Region
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</Form>
|
||||
{selectedRegion === "ALL" ? (
|
||||
<RegionSummaryTable
|
||||
regionProgress={regionProgress.data}
|
||||
/>
|
||||
) : (
|
||||
<MultiDepartmentProgressDisplay
|
||||
departments={regionProgress.data[0].departments}
|
||||
monthRange={regionProgress.data[0].monthRange}
|
||||
label={regionProgress.data[0].region.name}
|
||||
showDetails
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Alert color="warning">
|
||||
Unable to load your region information.
|
||||
</Alert>
|
||||
)}
|
||||
</AsyncGuard>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionDashboard;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import ActivityService from "../../shared/services/ActivityService";
|
||||
import { IProgressTotal, IRegionProgress } from "../../shared/models/Activity";
|
||||
import ProductTableHeader from "../../components/shared/ProductTableHeader";
|
||||
import ProgressTableRow from "../../components/shared/ProgressTableRow";
|
||||
import { IRegion } from "../../shared/models/Settings";
|
||||
import ProgressPercentageTableRow from "../../components/shared/ProgressPercentageTableRow";
|
||||
|
||||
export interface IRegionSummaryArgs {
|
||||
regionProgress: IRegionProgress[];
|
||||
}
|
||||
const RegionSummaryTable: React.FC<IRegionSummaryArgs> = (props) => {
|
||||
const regionTotals: IProgressTotal[] = [];
|
||||
const regions: IRegion[] = [];
|
||||
props.regionProgress.forEach((region) => {
|
||||
const totals = ActivityService.calculateTotalProgress(
|
||||
region.departments
|
||||
);
|
||||
regionTotals.push(totals);
|
||||
regions.push(region.region);
|
||||
});
|
||||
|
||||
const allRegionTotal = ActivityService.joinTotalProgress(regionTotals);
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped tablecenterCSS tablecenterheadCSS"
|
||||
data-test="multi-department-goal-progress-table-view"
|
||||
>
|
||||
<ProductTableHeader />
|
||||
<tbody>
|
||||
{regionTotals.map((eachTotal, idx) => (
|
||||
<ProgressTableRow
|
||||
label={regions[idx].name}
|
||||
progress={eachTotal.activityTotal}
|
||||
goals={eachTotal.goalTotal}
|
||||
/>
|
||||
))}
|
||||
<ProgressPercentageTableRow
|
||||
label="Goal Progress"
|
||||
progress={allRegionTotal.activityTotal}
|
||||
goals={allRegionTotal.goalTotal}
|
||||
/>
|
||||
<ProgressTableRow
|
||||
label="Totals"
|
||||
progress={allRegionTotal.activityTotal}
|
||||
goals={allRegionTotal.goalTotal}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionSummaryTable;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { shallow } from "enzyme";
|
||||
import * as React from "react";
|
||||
import * as reactRedux from "react-redux";
|
||||
import DeadActivityPage from "./DeadActivityPage";
|
||||
import { IDeadActivityState } from "../../reducers/DeadActivityReducer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import DeadActivityAuditLog from "../../components/dead-activity/DeadActivityAuditLog";
|
||||
import DeadActivityPageFilterForm from "./DeadActivityPageFilterForm";
|
||||
|
||||
describe("A component for finding activities", () => {
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useEffectMock = jest.spyOn(React, "useEffect");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
useEffectMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a blank search form", () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
const mockActivities: IDeadActivityState = {
|
||||
filteredDeadActivities: {
|
||||
state: AsyncState.Inactive
|
||||
},
|
||||
deadActivityStateFilter: []
|
||||
};
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(mockActivities);
|
||||
const wrapper = shallow(<DeadActivityPage />);
|
||||
|
||||
const found = wrapper.find(DeadActivityPageFilterForm);
|
||||
expect(found.length).toBe(1);
|
||||
|
||||
const found2 = wrapper.find(DeadActivityAuditLog);
|
||||
expect(found2.length).toBe(1);
|
||||
|
||||
expect(dummyDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import React from "react";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Configuration from "./../../shared/configuration/Configuration";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { debounce } from "debounce";
|
||||
import { IDeadActivitySearchForm } from "../../shared/models/DeadActivity";
|
||||
import {
|
||||
getAllDeadActivities,
|
||||
getLastPageOfDeadActivities,
|
||||
getNextPageOfDeadActivities
|
||||
} from "../../actions/DeadActivityActions";
|
||||
import DeadActivityPageFilterForm from "./DeadActivityPageFilterForm";
|
||||
import DeadActivityAuditLog from "../../components/dead-activity/DeadActivityAuditLog";
|
||||
import { IDeadActivityState } from "../../reducers/DeadActivityReducer";
|
||||
|
||||
const ActivityAuditLogTranslate =
|
||||
Translate.Get.components.deadActivity.deadActivityAuditLog;
|
||||
|
||||
const DeadActivityPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [form, setForm] = React.useState<IDeadActivitySearchForm>({
|
||||
state: [],
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
query: "",
|
||||
product: "",
|
||||
type: ""
|
||||
});
|
||||
|
||||
const activityState = useSelector<IAppState, IDeadActivityState>(
|
||||
(state) => state.deadActivityState
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
search(form);
|
||||
}, [form]);
|
||||
|
||||
const search = React.useCallback(
|
||||
debounce((query: IDeadActivitySearchForm) => {
|
||||
dispatch(
|
||||
getAllDeadActivities(
|
||||
1,
|
||||
Configuration.Get.defaultPaginationLimit,
|
||||
query
|
||||
)
|
||||
);
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
const changeSort = (headerName: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
sortBy: headerName,
|
||||
sortDescending:
|
||||
headerName === form.sortBy ? !form.sortDescending : false
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>{ActivityAuditLogTranslate.auditTableTitle}</h5>
|
||||
<DeadActivityPageFilterForm filterResults={setForm} />
|
||||
<DeadActivityAuditLog
|
||||
deadActivityContainer={activityState.filteredDeadActivities}
|
||||
getNextPage={() => dispatch(getNextPageOfDeadActivities(form))}
|
||||
getPrevPage={() => dispatch(getLastPageOfDeadActivities(form))}
|
||||
refresh={() => search(form)}
|
||||
changeSort={changeSort}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeadActivityPage;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import DeadActivityPageFilterForm from "./DeadActivityPageFilterForm";
|
||||
import { IDeadActivitySearchForm } from "../../shared/models/DeadActivity";
|
||||
import { FormGroup } from "reactstrap";
|
||||
|
||||
describe("ActivePageFilterForm", () => {
|
||||
it("should display form correctly", () => {
|
||||
let testCalls = 0;
|
||||
const testCall = (filter: IDeadActivitySearchForm) => {
|
||||
testCalls++;
|
||||
};
|
||||
const wrapper = mount(
|
||||
<DeadActivityPageFilterForm filterResults={testCall} />
|
||||
);
|
||||
|
||||
expect(wrapper.find(FormGroup).length).toEqual(7);
|
||||
expect(testCalls).toEqual(1);
|
||||
});
|
||||
|
||||
it("should respond to changes correctly", () => {
|
||||
let testCalls = 0;
|
||||
let lastFilter: IDeadActivitySearchForm = {
|
||||
query: "",
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
state: [],
|
||||
product: "",
|
||||
type: "",
|
||||
employeeId: "",
|
||||
departmentId: 0
|
||||
};
|
||||
const testCall = (filter: IDeadActivitySearchForm) => {
|
||||
lastFilter = filter;
|
||||
testCalls++;
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DeadActivityPageFilterForm filterResults={testCall} />
|
||||
);
|
||||
expect(wrapper.find(FormGroup).length).toEqual(7);
|
||||
|
||||
wrapper
|
||||
.find("[name='query']")
|
||||
.first()
|
||||
.simulate("change", { target: { value: "3" } });
|
||||
expect(testCalls).toEqual(2);
|
||||
expect(lastFilter.query).toEqual("3");
|
||||
|
||||
wrapper
|
||||
.find("[name='ActivityProduct']")
|
||||
.first()
|
||||
.simulate("change", { target: { value: "Vehicle" } });
|
||||
expect(testCalls).toEqual(3);
|
||||
expect(lastFilter.product).toEqual("Vehicle");
|
||||
expect(lastFilter.query).toEqual("3");
|
||||
|
||||
wrapper
|
||||
.find("[name='ActivityType']")
|
||||
.first()
|
||||
.simulate("change", { target: { value: "Sale" } });
|
||||
expect(testCalls).toEqual(4);
|
||||
expect(lastFilter.type).toEqual("Sale");
|
||||
expect(lastFilter.product).toEqual("Vehicle");
|
||||
expect(lastFilter.query).toEqual("3");
|
||||
});
|
||||
});
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormFeedback, FormGroup, Input, Label } from "reactstrap";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import moment, { Moment } from "moment";
|
||||
import Translate from "../../translate/Translate";
|
||||
import {
|
||||
ActivityType,
|
||||
IActivitySearchForm
|
||||
} from "../../shared/models/Activity";
|
||||
import UserSearchSelect from "../../components/activity/manual-activity-wizard/UserSearchSelect";
|
||||
|
||||
interface IDeadActivityPageFilterForm {
|
||||
filterResults: (filter: IActivitySearchForm) => void;
|
||||
}
|
||||
|
||||
const activityFilterFormTranslate =
|
||||
Translate.Get.components.deadActivity.deadActivityFilterForm;
|
||||
|
||||
const DeadActivityPageFilterForm: React.FC<IDeadActivityPageFilterForm> = ({
|
||||
filterResults
|
||||
}) => {
|
||||
const [form, setForm] = useState<IActivitySearchForm>({
|
||||
state: [],
|
||||
dateHigh: null,
|
||||
dateLow: null,
|
||||
query: "",
|
||||
product: "",
|
||||
type: "",
|
||||
departmentId: 0,
|
||||
employeeId: ""
|
||||
});
|
||||
|
||||
const settings = React.useContext(SettingsContext);
|
||||
useEffect(() => {
|
||||
filterResults(form);
|
||||
}, [form]);
|
||||
const departments = settings?.departments || [];
|
||||
|
||||
const updateProduct = (newProduct: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
product: newProduct
|
||||
});
|
||||
};
|
||||
|
||||
const updateType = (newType: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
type: newType
|
||||
});
|
||||
};
|
||||
|
||||
const setSearchQuery = (newQuery: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
query: newQuery
|
||||
});
|
||||
};
|
||||
|
||||
const updateLowDate = (date: Moment) => {
|
||||
setForm({
|
||||
...form,
|
||||
dateLow: date
|
||||
});
|
||||
};
|
||||
|
||||
const updateHighDate = (date: Moment) => {
|
||||
setForm({
|
||||
...form,
|
||||
dateHigh: date
|
||||
});
|
||||
};
|
||||
|
||||
const updateDepartment = (departmentId: number) => {
|
||||
setForm({
|
||||
...form,
|
||||
departmentId: departmentId
|
||||
});
|
||||
};
|
||||
|
||||
const updateEmployee = (employeeId?: string) => {
|
||||
setForm({
|
||||
...form,
|
||||
employeeId: employeeId
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.productType.label}</Label>
|
||||
<Input
|
||||
name="ActivityProduct"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={form.product === "" ? "" : form.product}
|
||||
onChange={(event) => {
|
||||
updateProduct(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" key="default_prod_type_option">
|
||||
{activityFilterFormTranslate.productType.default}
|
||||
</option>
|
||||
{settings?.productDetails.map((product) => {
|
||||
return (
|
||||
<option
|
||||
key={product.product}
|
||||
value={product.product}
|
||||
>
|
||||
{product.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.activityType.label}</Label>
|
||||
<Input
|
||||
name="ActivityType"
|
||||
type="select"
|
||||
bsSize="sm"
|
||||
value={form.type === "" ? "" : form.type}
|
||||
onChange={(event) => {
|
||||
updateType(event.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="" key="default_activity_state">
|
||||
{activityFilterFormTranslate.activityType.default}
|
||||
</option>
|
||||
{Object.values(ActivityType).map((activityType) => {
|
||||
return (
|
||||
<option key={activityType}>{activityType}</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>
|
||||
{activityFilterFormTranslate.departmentId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="CreditToDepartment"
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="dept-input"
|
||||
value={form.departmentId}
|
||||
onChange={(event) => {
|
||||
updateDepartment(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-option"
|
||||
key={undefined}
|
||||
value=""
|
||||
>
|
||||
{activityFilterFormTranslate.departmentId.placeHolder}
|
||||
</option>
|
||||
{departments.map((department) => {
|
||||
return (
|
||||
<option key={department.id} value={department.id}>
|
||||
{department.departmentName}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
<FormFeedback>Please select a value.</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-6 col-lg-4">
|
||||
<Label>
|
||||
{activityFilterFormTranslate.employeeId.label}
|
||||
<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<UserSearchSelect
|
||||
data-test="CreditToAssociate"
|
||||
employeeId={form.employeeId ?? ""}
|
||||
onUserSelected={(field, value) => {
|
||||
updateEmployee(value);
|
||||
}}
|
||||
showValidation={false}
|
||||
placeHolder={
|
||||
activityFilterFormTranslate.employeeId.placeHolder
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.lowDate.label}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
data-test="low-date-input"
|
||||
value={form.dateLow?.format("YYYY-MM-DD").toString() ?? ""}
|
||||
invalid={false}
|
||||
onChange={(event) => {
|
||||
updateLowDate(moment.utc(event.target.value));
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
DeptAssignmentTranslate.validation.endDate
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.highDate.label}</Label>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
data-test="high-date-input"
|
||||
value={form.dateHigh?.format("YYYY-MM-DD").toString() ?? ""}
|
||||
invalid={false}
|
||||
onChange={(event) => {
|
||||
updateHighDate(moment.utc(event.target.value));
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
DeptAssignmentTranslate.validation.endDate
|
||||
</FormFeedback>
|
||||
</FormGroup>
|
||||
<FormGroup className="col-sm-12 col-md-4 col-lg-2">
|
||||
<Label>{activityFilterFormTranslate.searchQuery.label}</Label>
|
||||
<Input
|
||||
name="query"
|
||||
bsSize="sm"
|
||||
value={form.query}
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.target.value);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={
|
||||
activityFilterFormTranslate.searchQuery.placeHolder
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeadActivityPageFilterForm;
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import ReactRouter from "react-router";
|
||||
import DepartmentDetailPage from "./DepartmentDetailPage";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { getMockAppSettings } from "../../shared/testing/mocks/MockAppSettings";
|
||||
import { getMockDepartments } from "../../shared/testing/mocks/MockDepartments";
|
||||
import DepartmentService from "../../shared/services/DepartmentService";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
import moment from "moment";
|
||||
|
||||
const pageTranslate = Translate.Get.containers.departmentDetailPage;
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
|
||||
beforeEach(() => {
|
||||
useContextMock.mockClear();
|
||||
});
|
||||
|
||||
describe("DepartmentDetailPage", () => {
|
||||
it("should display unavailable", async () => {
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "9999999" });
|
||||
jest.spyOn(DepartmentService, "getDepartmentDetails").mockResolvedValue(
|
||||
{
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
assignments: []
|
||||
}
|
||||
}
|
||||
);
|
||||
let wrapper: any;
|
||||
await act(async () => {
|
||||
wrapper = mount(<DepartmentDetailPage />);
|
||||
});
|
||||
|
||||
expect(wrapper.find("[data-test='department-name']").length).toEqual(0);
|
||||
expect(wrapper.html()).toEqual(pageTranslate.unavailable);
|
||||
});
|
||||
|
||||
it("should display the department details", async () => {
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "55" });
|
||||
jest.spyOn(DepartmentService, "getDepartmentDetails").mockResolvedValue(
|
||||
{
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
assignments: []
|
||||
}
|
||||
}
|
||||
);
|
||||
let wrapper: any;
|
||||
const settings = getMockAppSettings();
|
||||
settings.departments = getMockDepartments();
|
||||
useContextMock.mockReturnValue(settings);
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<DepartmentDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-display']").length
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it("should display personnel", async () => {
|
||||
jest.spyOn(ReactRouter, "useParams").mockReturnValue({ id: "55" });
|
||||
const manager = mockUser();
|
||||
const departmentAssignment = {
|
||||
departmentId: 55,
|
||||
role: {
|
||||
name: "Manager",
|
||||
description: "manager",
|
||||
permissions: [],
|
||||
hasCommissions: false
|
||||
},
|
||||
employee: manager,
|
||||
employeeId: manager.employeeId,
|
||||
isHome: true,
|
||||
note: "Manager",
|
||||
slidingPayout: 100,
|
||||
startDate: moment()
|
||||
};
|
||||
manager.departments;
|
||||
jest.spyOn(DepartmentService, "getDepartmentDetails").mockResolvedValue(
|
||||
{
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
assignments: [departmentAssignment]
|
||||
}
|
||||
}
|
||||
);
|
||||
let wrapper: any;
|
||||
const settings = getMockAppSettings();
|
||||
settings.departments = getMockDepartments();
|
||||
useContextMock.mockReturnValue(settings);
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mount(<DepartmentDetailPage />);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find("[data-test='no-department-detail-assignments']")
|
||||
.length
|
||||
).toEqual(1);
|
||||
|
||||
expect(
|
||||
wrapper.find("[data-test='department-detail-assignments']").length
|
||||
).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
const mockUser = () => {
|
||||
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
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [],
|
||||
assignments: []
|
||||
};
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { IDepartmentDetails } from "../../shared/models/Department";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import DepartmentService from "../../shared/services/DepartmentService";
|
||||
import DepartmentDetailDisplay from "../../components/department-detail/DepartmentDetailDisplay";
|
||||
import DepartmentDetailAssignments from "../../components/department-detail/DepartmentDetailAssignments";
|
||||
import ProfileService from "../../shared/services/ProfileService";
|
||||
import { Col, Row } from "reactstrap";
|
||||
import DepartmentDisplay, {
|
||||
DepartmentDisplayTargets
|
||||
} from "../shared/DepartmentDisplay";
|
||||
|
||||
const pageTranslate = Translate.Get.containers.departmentDetailPage;
|
||||
|
||||
const DepartmentDetailPage: React.FC = () => {
|
||||
const [details, setDetails] = useState<
|
||||
IAsyncStateContainer<IDepartmentDetails>
|
||||
>({ state: AsyncState.Inactive });
|
||||
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const departments = settings?.departments;
|
||||
const department = departments?.find((x) => x.id.toString() === id);
|
||||
|
||||
useEffect(() => {
|
||||
loadDepartments();
|
||||
}, []);
|
||||
|
||||
const loadDepartments = async () => {
|
||||
setDetails({ state: AsyncState.Loading });
|
||||
const details = await DepartmentService.getDepartmentDetails(+id);
|
||||
setDetails(details);
|
||||
};
|
||||
|
||||
if (!department) {
|
||||
return <>{pageTranslate.unavailable}</>;
|
||||
}
|
||||
|
||||
const managers = details.data?.assignments.filter((x) =>
|
||||
ProfileService.isDepartmentManager(x.employee, department.id)
|
||||
);
|
||||
const personnel = details.data?.assignments.filter(
|
||||
(x) => !ProfileService.isDepartmentManager(x.employee, department.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
{pageTranslate.title} - {department.departmentName}
|
||||
</h3>
|
||||
<Row>
|
||||
<Col lg="3">
|
||||
<div style={{ display: "inline-flex" }}>
|
||||
<h5>{pageTranslate.details} </h5>
|
||||
<h5>
|
||||
<DepartmentDisplay
|
||||
departmentId={department.id}
|
||||
target={DepartmentDisplayTargets.Dashboard}
|
||||
nameOverride={pageTranslate.progress}
|
||||
/>
|
||||
</h5>
|
||||
</div>
|
||||
{details.data && (
|
||||
<DepartmentDetailDisplay
|
||||
department={department}
|
||||
details={details.data}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col>
|
||||
<h5>{pageTranslate.managers}</h5>
|
||||
<DepartmentDetailAssignments assignments={managers} />
|
||||
|
||||
<h5 className="mt-2">{pageTranslate.personnel}</h5>
|
||||
<DepartmentDetailAssignments assignments={personnel} />
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentDetailPage;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { Alert, Spinner } from "reactstrap";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
|
||||
export interface IEmployeeDetailsEditAlertBarProps {
|
||||
submitStatusMessage: string;
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
const EmployeeDetailsEditAlertBar: React.FC<IEmployeeDetailsEditAlertBarProps> = (
|
||||
props
|
||||
) => {
|
||||
if (props.submitState.state == AsyncState.Success) {
|
||||
return (
|
||||
<Alert className="py-2 mx-1 mb-0" color="success">
|
||||
<p className="mb-0">{props.submitStatusMessage}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.submitState.state == AsyncState.Loading) {
|
||||
return (
|
||||
<Alert
|
||||
className="py-1 mx-3 mb-0 d-flex flex-row align-items-center"
|
||||
color="warning"
|
||||
>
|
||||
<Spinner className="mr-2" size="sm" />
|
||||
<p className="mb-0">{props.submitStatusMessage}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.submitState.state == AsyncState.Failure) {
|
||||
return (
|
||||
<Alert
|
||||
className="d-flex mb-0 flex-row align-items-center py-1 mx-3"
|
||||
color="danger"
|
||||
>
|
||||
<span className="fa fa-times-circle mr-2" />
|
||||
<p className="mb-0">{props.submitStatusMessage}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default EmployeeDetailsEditAlertBar;
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup, Input } from "reactstrap";
|
||||
import Translate from "../../translate/Translate";
|
||||
import {
|
||||
IEmployee,
|
||||
IEmployeeDetailsUpdate
|
||||
} from "../../shared/models/Employee";
|
||||
import { tryUpdateStateWithInput } from "../../shared/services/InputService";
|
||||
|
||||
export interface IEmployeeDetailsEditProps {
|
||||
profile: IEmployee;
|
||||
employeeDetailsUpdateResult: string;
|
||||
formState: IEmployeeDetailsUpdate;
|
||||
submitEmployeeDetailsUpdate: (update: IEmployeeDetailsUpdate) => void;
|
||||
disableEdit: () => void;
|
||||
updateFormState: (newState: IEmployeeDetailsUpdate) => void;
|
||||
}
|
||||
|
||||
const profileTranslate = Translate.Get.components.profileEdit;
|
||||
|
||||
const EmployeeDetailsEdit: React.FC<IEmployeeDetailsEditProps> = (props) => {
|
||||
const submitForm = () => {
|
||||
props.submitEmployeeDetailsUpdate(props.formState);
|
||||
props.disableEdit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test="profile-page-wrapper" style={{ width: "100%" }}>
|
||||
<h6>{profileTranslate.header}</h6>
|
||||
<table
|
||||
className="table striped"
|
||||
style={{ width: "100%" }}
|
||||
data-test="profile-page-table"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={3}>{profileTranslate.employeeEmail}</td>
|
||||
<td>
|
||||
<Input
|
||||
name="newEmail"
|
||||
bsSize="sm"
|
||||
value={props.formState.newEmail}
|
||||
onChange={(event) => {
|
||||
props.updateFormState(
|
||||
tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
"newEmail",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={props.formState.newEmail}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3}>{profileTranslate.employeeId}</td>
|
||||
<td>
|
||||
<Input
|
||||
name="newEmployeeId"
|
||||
bsSize="sm"
|
||||
value={props.formState.newEmployeeId}
|
||||
onChange={(event) => {
|
||||
props.updateFormState(
|
||||
tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
"newEmployeeId",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={props.formState.newEmployeeId}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3}>{profileTranslate.employeeFullName}</td>
|
||||
<td>
|
||||
<Input
|
||||
name="newEmployeeName"
|
||||
bsSize="sm"
|
||||
value={props.formState.newFullName}
|
||||
onChange={(event) => {
|
||||
props.updateFormState(
|
||||
tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
"newFullName",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={props.formState.newFullName}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
submitForm();
|
||||
}}
|
||||
data-test="submit-button"
|
||||
>
|
||||
{profileTranslate.submitButton}
|
||||
</Button>
|
||||
<Button
|
||||
className="m-0"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
props.disableEdit();
|
||||
}}
|
||||
data-test="clear-button"
|
||||
>
|
||||
<span className="fa fa-times" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeDetailsEdit;
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import React, { useState } from "react";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { Button, FormFeedback } from "reactstrap";
|
||||
import { IEmployeeDetailsUpdate } from "../../shared/models/Employee";
|
||||
import EmployeeDetailsEdit from "./EmployeeDetailsEditPage";
|
||||
import EmployeeDetailsEditAlertBar from "./EmployeeDetailsEditAlertBar";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
|
||||
export interface IEmployeeDetailsPageParams {
|
||||
profile: IEmployee;
|
||||
employeeDetailsUpdateResult: string;
|
||||
submitStatus: IAsyncStateContainer<string>;
|
||||
refreshProfile: () => void;
|
||||
submitEmployeeDetailsUpdate: (update: IEmployeeDetailsUpdate) => void;
|
||||
}
|
||||
|
||||
const profileTranslate = Translate.Get.components.profileEdit;
|
||||
|
||||
const EmployeeDetailsPage: React.FC<IEmployeeDetailsPageParams> = (props) => {
|
||||
const profile = props.profile;
|
||||
|
||||
const createFormStateFromAssignment = (
|
||||
employee: IEmployee
|
||||
): IEmployeeDetailsUpdate => {
|
||||
return {
|
||||
oldEmail: employee.adEmail,
|
||||
oldEmployeeId: employee.employeeId,
|
||||
oldFullName: employee.fullName,
|
||||
newEmail: employee.adEmail,
|
||||
newEmployeeId: employee.employeeId,
|
||||
newFullName: employee.fullName
|
||||
};
|
||||
};
|
||||
|
||||
const initialFormState = createFormStateFromAssignment(props.profile);
|
||||
const [isEditing, setEdit] = useState<boolean>(false);
|
||||
const [formState, setFormState] = useState<IEmployeeDetailsUpdate>(
|
||||
initialFormState
|
||||
);
|
||||
|
||||
const enableEdit = () => {
|
||||
setFormState(createFormStateFromAssignment(props.profile));
|
||||
setEdit(true);
|
||||
};
|
||||
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<div data-test="profile-page-wrapper" style={{ width: "100%" }}>
|
||||
<h6>{profileTranslate.header}</h6>
|
||||
<FormFeedback>{props.employeeDetailsUpdateResult}</FormFeedback>
|
||||
<table
|
||||
className="table striped"
|
||||
style={{ width: "100%" }}
|
||||
data-test="profile-page-table"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{profileTranslate.employeeEmail}</td>
|
||||
<td colSpan={2}>{profile.adEmail}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{profileTranslate.employeeId}</td>
|
||||
<td>{profile.employeeId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{profileTranslate.employeeFullName}</td>
|
||||
<td>{profile.fullName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => enableEdit()}
|
||||
>
|
||||
{profileTranslate.edit}
|
||||
</Button>
|
||||
</td>
|
||||
<td>
|
||||
<EmployeeDetailsEditAlertBar
|
||||
submitStatusMessage={
|
||||
props.employeeDetailsUpdateResult
|
||||
}
|
||||
submitState={props.submitStatus}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EmployeeDetailsEdit
|
||||
formState={formState}
|
||||
profile={props.profile}
|
||||
employeeDetailsUpdateResult={props.employeeDetailsUpdateResult}
|
||||
submitEmployeeDetailsUpdate={props.submitEmployeeDetailsUpdate}
|
||||
disableEdit={() => setEdit(false)}
|
||||
updateFormState={(newState) => setFormState(newState)}
|
||||
data-test="edit-row"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeeDetailsPage;
|
||||
@@ -0,0 +1,90 @@
|
||||
import "../../components/tab-view/TabView.css";
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
submitDepartmentAssignment,
|
||||
submitDepartmentAssignmentUpdate,
|
||||
trySelectEmployee
|
||||
} from "../../actions/EmployeeActions";
|
||||
import { IEmployeeState } from "./../../reducers/EmployeeReducer";
|
||||
import { IAppState } from "./../../store/Store";
|
||||
import AsyncState from "./../../shared/enumerations/AsyncState";
|
||||
import EmployeeAlertBar from "./../../components/employee/EmployeeAlertBar";
|
||||
import DeptAssignmentTabGroup from "./department-assignments/DeptAssignmentTabGroup";
|
||||
import {
|
||||
IDepartmentAssignmentDto,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../../shared/models/DepartmentAssignment";
|
||||
import EmployeeService from "../../shared/services/EmployeeService";
|
||||
|
||||
export interface IEmployeeParam {
|
||||
employeeId: string;
|
||||
}
|
||||
|
||||
const EmployeePage: React.FC<IEmployeeParam> = (props) => {
|
||||
const employeeId = props.employeeId;
|
||||
const dispatch = useDispatch();
|
||||
const employeeState = useSelector<IAppState, IEmployeeState>(
|
||||
(state) => state.employeeState
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(trySelectEmployee(employeeId));
|
||||
}, []);
|
||||
|
||||
const submitNewDeptAssignment = async (
|
||||
deptAssignment: Partial<IDepartmentAssignmentDto>
|
||||
) => {
|
||||
if (employeeState.selectedEmployee?.data) {
|
||||
await dispatch(
|
||||
submitDepartmentAssignment(
|
||||
employeeState.selectedEmployee.data.employeeId,
|
||||
deptAssignment
|
||||
)
|
||||
);
|
||||
await dispatch(trySelectEmployee(employeeId));
|
||||
}
|
||||
};
|
||||
|
||||
const submitDeptAssignmentUpdate = async (
|
||||
update: IDepartmentAssignmentUpdateDto
|
||||
) => {
|
||||
update.employeeId = employeeId;
|
||||
if (
|
||||
employeeState.selectedEmployee?.data &&
|
||||
EmployeeService.isValidDepartmentAssignmentUpdate(update)
|
||||
) {
|
||||
await dispatch(submitDepartmentAssignmentUpdate(update));
|
||||
await dispatch(trySelectEmployee(employeeId));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<EmployeeAlertBar
|
||||
employeeContainer={
|
||||
employeeState.selectedEmployee ?? {
|
||||
state: AsyncState.Inactive
|
||||
}
|
||||
}
|
||||
searchEmployeeId={employeeId}
|
||||
/>
|
||||
<div className="p-2">
|
||||
<DeptAssignmentTabGroup
|
||||
employeeDepartmentAssignments={
|
||||
employeeState.selectedEmployee?.data?.departments ?? []
|
||||
}
|
||||
submitState={employeeState.submitState}
|
||||
submitNewDeptAssignment={(assignmentData) =>
|
||||
submitNewDeptAssignment(assignmentData)
|
||||
}
|
||||
submitDeptAssignmentUpdate={(
|
||||
deptUpdate: IDepartmentAssignmentUpdateDto
|
||||
) => submitDeptAssignmentUpdate(deptUpdate)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeePage;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import Translate from "../../../translate/Translate";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { IAppState } from "../../../store/Store";
|
||||
import { IDepartmentState } from "../../../reducers/DepartmentReducer";
|
||||
import { AutoTable } from "../../../components/auto-table/AutoTable";
|
||||
import React, { useEffect } from "react";
|
||||
import { getAllDepartments } from "../../../actions/DepartmentActions";
|
||||
import moment from "moment";
|
||||
import { IDepartmentAssignment } from "../../../shared/models/DepartmentAssignment";
|
||||
|
||||
export interface IDepartmentAssignmentTableProps {
|
||||
assignments: IDepartmentAssignment[];
|
||||
}
|
||||
|
||||
const departmentAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTable;
|
||||
|
||||
const BasicDeptAssignmentTable: React.FC<IDepartmentAssignmentTableProps> = (
|
||||
props
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const departmentState = useSelector<IAppState, IDepartmentState>(
|
||||
(state) => state.departmentState
|
||||
);
|
||||
|
||||
const getDepartmentNameFromId = (id: number): string => {
|
||||
return (
|
||||
departmentState.departments.data?.find((dept) => dept.id === id)
|
||||
?.departmentName ?? "-"
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllDepartments());
|
||||
}, []);
|
||||
|
||||
const headers = [
|
||||
departmentAssignmentTranslate.headers.isHome,
|
||||
departmentAssignmentTranslate.headers.departmentId,
|
||||
departmentAssignmentTranslate.headers.note,
|
||||
departmentAssignmentTranslate.headers.startDate,
|
||||
departmentAssignmentTranslate.headers.endDate
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IDepartmentAssignment) => (x.isHome ? "Home Dept" : ""),
|
||||
(x: IDepartmentAssignment) => getDepartmentNameFromId(x.departmentId),
|
||||
(x: IDepartmentAssignment) => x.note,
|
||||
(x: IDepartmentAssignment) =>
|
||||
moment(x.startDate).format("MM-DD-YYYY").toString(),
|
||||
(x: IDepartmentAssignment) =>
|
||||
x.endDate
|
||||
? moment(x.endDate).format("MM-DD-YYYY").toString()
|
||||
: "active"
|
||||
];
|
||||
return (
|
||||
<AutoTable
|
||||
className="border rounded"
|
||||
headers={headers}
|
||||
sortedColumn={{ index: 4, isReversed: false }}
|
||||
cells={cells}
|
||||
elements={props.assignments}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default BasicDeptAssignmentTable;
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { Alert, Spinner } from "reactstrap";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import AsyncState from "./../../../shared/enumerations/AsyncState";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
|
||||
export interface IDeptAssignmentAlertBarProps {
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
}
|
||||
|
||||
const alertBarTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentAlertBar;
|
||||
|
||||
const DeptAssignmentAlertBar: React.FC<IDeptAssignmentAlertBarProps> = (
|
||||
props
|
||||
) => {
|
||||
if (props.submitState.state == AsyncState.Success) {
|
||||
return (
|
||||
<Alert className="py-2 mx-1 mb-0" color="success">
|
||||
<p className="mb-0">{alertBarTranslate.success}</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.submitState.state == AsyncState.Loading) {
|
||||
return (
|
||||
<Alert
|
||||
className="py-1 mx-3 mb-0 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 mb-0 flex-row align-items-center py-1 mx-3"
|
||||
color="danger"
|
||||
>
|
||||
<span className="fa fa-times-circle mr-2" />
|
||||
<p className="mb-0">
|
||||
{alertBarTranslate.error} {props.submitState.data}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default DeptAssignmentAlertBar;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import Translate from "../../../translate/Translate";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { IAppState } from "../../../store/Store";
|
||||
import { IDepartmentState } from "../../../reducers/DepartmentReducer";
|
||||
import { AutoTable } from "../../../components/auto-table/AutoTable";
|
||||
import React, { useEffect } from "react";
|
||||
import { getAllDepartments } from "../../../actions/DepartmentActions";
|
||||
import moment from "moment";
|
||||
import HomeIcon from "./HomeIcon";
|
||||
import { IDepartmentAssignment } from "../../../shared/models/DepartmentAssignment";
|
||||
|
||||
export interface IDeptAssignmentAuditTable {
|
||||
assignments: IDepartmentAssignment[];
|
||||
}
|
||||
|
||||
const departmentAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment
|
||||
.deptAssignmentAuditTable;
|
||||
|
||||
const DeptAssignmentAuditTable: React.FC<IDeptAssignmentAuditTable> = (
|
||||
props
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const departmentState = useSelector<IAppState, IDepartmentState>(
|
||||
(state) => state.departmentState
|
||||
);
|
||||
|
||||
const getDepartmentNameFromId = (id: number): string => {
|
||||
return (
|
||||
departmentState.departments.data?.find((dept) => dept.id === id)
|
||||
?.departmentName ?? "-"
|
||||
);
|
||||
};
|
||||
|
||||
const sortDataAudits = (
|
||||
a: IDepartmentAssignment,
|
||||
b: IDepartmentAssignment
|
||||
): number => {
|
||||
const momenta = moment(a.lastUpdated);
|
||||
const momentb = moment(b.lastUpdated);
|
||||
return momenta.isBefore(momentb)
|
||||
? 1
|
||||
: momenta.isAfter(momentb)
|
||||
? -1
|
||||
: 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllDepartments());
|
||||
}, []);
|
||||
|
||||
const headers = [
|
||||
departmentAssignmentTranslate.headers.isHome,
|
||||
departmentAssignmentTranslate.headers.departmentId,
|
||||
departmentAssignmentTranslate.headers.note,
|
||||
departmentAssignmentTranslate.headers.startDate,
|
||||
departmentAssignmentTranslate.headers.endDate,
|
||||
departmentAssignmentTranslate.headers.created,
|
||||
departmentAssignmentTranslate.headers.createdBy,
|
||||
departmentAssignmentTranslate.headers.createdByType,
|
||||
departmentAssignmentTranslate.headers.lastUpdated,
|
||||
departmentAssignmentTranslate.headers.lastUpdatedBy,
|
||||
departmentAssignmentTranslate.headers.UpdatedByType
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IDepartmentAssignment) => <HomeIcon isHome={x.isHome}></HomeIcon>,
|
||||
(x: IDepartmentAssignment) => getDepartmentNameFromId(x.departmentId),
|
||||
(x: IDepartmentAssignment) => x.note,
|
||||
(x: IDepartmentAssignment) =>
|
||||
moment(x.startDate).format("MM-DD-YYY").toString(),
|
||||
(x: IDepartmentAssignment) =>
|
||||
x.endDate
|
||||
? moment(x.endDate).format("MM-DD-YYY").toString()
|
||||
: "active",
|
||||
(x: IDepartmentAssignment) => moment(x.created).toISOString(),
|
||||
(x: IDepartmentAssignment) => x.createdBy,
|
||||
(x: IDepartmentAssignment) => x.createdByType,
|
||||
(x: IDepartmentAssignment) => moment(x.lastUpdated).toISOString(),
|
||||
(x: IDepartmentAssignment) => x.lastUpdatedBy,
|
||||
(x: IDepartmentAssignment) => x.lastUpdatedByType
|
||||
];
|
||||
return (
|
||||
<AutoTable
|
||||
className="border rounded"
|
||||
headers={headers}
|
||||
cells={cells}
|
||||
sortedColumn={{ index: 8, isReversed: false }}
|
||||
elements={props.assignments.sort((a, b) => {
|
||||
return sortDataAudits(a, b);
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default DeptAssignmentAuditTable;
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import DeptAssignmentTabGroup from "./DeptAssignmentTabGroup";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IDepartmentState } from "../../../reducers/DepartmentReducer";
|
||||
import PermissionCode from "../../../shared/enumerations/PermissionCode";
|
||||
import { IProfileState } from "../../../reducers/ProfileReducer";
|
||||
import { mockDeptAssignments } from "../../../shared/testing/mocks/MockDepartmentAssignments";
|
||||
import { mockRoles } from "../../../shared/testing/mocks/MockRoles";
|
||||
import { ISettings } from "../../../shared/models/Settings";
|
||||
import { getMockDepartments } from "../../../shared/testing/mocks/MockDepartments";
|
||||
import { IDepartmentAssignment } from "../../../shared/models/DepartmentAssignment";
|
||||
|
||||
const mockSettingsState: ISettings = {
|
||||
appRoles: mockRoles,
|
||||
permissions: [],
|
||||
productDetails: [],
|
||||
businessLines: [],
|
||||
productSet: [],
|
||||
regions: [],
|
||||
payoutProfiles: [],
|
||||
activityStateSet: {},
|
||||
departmentStateSet: {},
|
||||
activityTypeSet: {},
|
||||
organizationSet: {},
|
||||
systemTypeSet: {},
|
||||
userTypeSet: {},
|
||||
departments: getMockDepartments()
|
||||
};
|
||||
|
||||
const mockDepartmentState: IDepartmentState = {
|
||||
departments: {
|
||||
data: getMockDepartments(),
|
||||
state: AsyncState.Success
|
||||
},
|
||||
submitState: { state: AsyncState.Inactive }
|
||||
};
|
||||
|
||||
// setup appUser to get past permission guard
|
||||
const mockProfileState: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
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: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
|
||||
beforeEach(() => {
|
||||
useDispatchMock.mockClear();
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
|
||||
describe("User wants to switch views of department assignments", () => {
|
||||
it("Only displays active department assignments in the active tab and sorts by end date desc", () => {
|
||||
useDispatchMock.mockReturnValue(jest.fn());
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentTabGroup
|
||||
submitNewDeptAssignment={() => jest.fn()}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
submitState={{ state: AsyncState.Inactive }}
|
||||
employeeDepartmentAssignments={mockDeptAssignments}
|
||||
/>
|
||||
);
|
||||
const assignments: [] = wrapper
|
||||
.find("DeptAssignmentTable")
|
||||
.prop("assignments");
|
||||
expect(assignments.length).toBe(2);
|
||||
expect(assignments.map((x: IDepartmentAssignment) => x.id)).toEqual([
|
||||
2,
|
||||
1
|
||||
]);
|
||||
});
|
||||
|
||||
it("Only displays inactive department assignments in the closed tab and sorts by end date desc", () => {
|
||||
useDispatchMock.mockReturnValue(jest.fn());
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState,
|
||||
...mockSettingsState
|
||||
});
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentTabGroup
|
||||
submitNewDeptAssignment={() => jest.fn()}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
submitState={{ state: AsyncState.Inactive }}
|
||||
employeeDepartmentAssignments={mockDeptAssignments}
|
||||
/>
|
||||
);
|
||||
//make tab group display 2nd tab
|
||||
wrapper.find('[data-test="nav-link-1"]').simulate("click");
|
||||
wrapper.update();
|
||||
const assignments: [] = wrapper
|
||||
.find("BasicDeptAssignmentTable")
|
||||
.prop("assignments");
|
||||
expect(assignments.length).toBe(2);
|
||||
expect(assignments.map((x: IDepartmentAssignment) => x.id)).toEqual([
|
||||
3,
|
||||
0
|
||||
]);
|
||||
});
|
||||
|
||||
it("Displays everything in the audit", () => {
|
||||
useDispatchMock.mockReturnValue(jest.fn());
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState,
|
||||
...mockSettingsState
|
||||
});
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentTabGroup
|
||||
submitNewDeptAssignment={() => jest.fn()}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
submitState={{ state: AsyncState.Inactive }}
|
||||
employeeDepartmentAssignments={mockDeptAssignments}
|
||||
/>
|
||||
);
|
||||
//make tab group display 3rd tab
|
||||
wrapper.find('[data-test="nav-link-2"]').simulate("click");
|
||||
wrapper.update();
|
||||
const assignments: [] = wrapper
|
||||
.find("DeptAssignmentAuditTable")
|
||||
.prop("assignments");
|
||||
expect(assignments.length).toBe(4);
|
||||
expect(assignments.map((x: IDepartmentAssignment) => x.id)).toEqual([
|
||||
2,
|
||||
3,
|
||||
1,
|
||||
0
|
||||
]);
|
||||
});
|
||||
});
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import React from "react";
|
||||
import TabView from "../../../components/tab-view/TabView";
|
||||
import DeptAssignmentTable from "./department-assignment-table/DeptAssignmentTable";
|
||||
import BasicDeptAssignmentTable from "./BasicDeptAssignmentTable";
|
||||
import DeptAssignmentAuditTable from "./DeptAssignmentAuditTable";
|
||||
import moment from "moment";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import Translate from "./../../../translate/Translate";
|
||||
import {
|
||||
IDepartmentAssignment,
|
||||
IDepartmentAssignmentDto,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../../../shared/models/DepartmentAssignment";
|
||||
|
||||
export interface IDeptAssignmentTabGroupProps {
|
||||
employeeDepartmentAssignments: IDepartmentAssignment[];
|
||||
submitState: IAsyncStateContainer<string>;
|
||||
submitNewDeptAssignment: (data: Partial<IDepartmentAssignmentDto>) => void;
|
||||
submitDeptAssignmentUpdate: (
|
||||
deptUpdate: IDepartmentAssignmentUpdateDto
|
||||
) => void;
|
||||
}
|
||||
|
||||
const TabViewTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTabGroup;
|
||||
|
||||
const DeptAssignmentTabGroup: React.FC<IDeptAssignmentTabGroupProps> = (
|
||||
props
|
||||
) => {
|
||||
const activeAssignments = (): IDepartmentAssignment[] => {
|
||||
return (
|
||||
props.employeeDepartmentAssignments
|
||||
.filter(
|
||||
(x) =>
|
||||
x.endDate == undefined ||
|
||||
moment(x.endDate).isAfter(moment.now())
|
||||
)
|
||||
.sort((a, b) =>
|
||||
b.endDate == undefined ||
|
||||
moment(b.endDate).isBefore(moment(a.endDate))
|
||||
? 1
|
||||
: a.endDate == undefined ||
|
||||
moment(b.endDate).isBefore(moment(a.endDate))
|
||||
? -1
|
||||
: 0
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
const inActiveAssignments = (): IDepartmentAssignment[] => {
|
||||
return (
|
||||
props.employeeDepartmentAssignments
|
||||
.filter(
|
||||
(x) =>
|
||||
x.endDate !== undefined &&
|
||||
moment(x.endDate).isBefore(moment.now())
|
||||
)
|
||||
.sort((a, b) =>
|
||||
moment(a.endDate).isBefore(b.endDate)
|
||||
? 1
|
||||
: moment(a.endDate).isAfter(b.endDate)
|
||||
? -1
|
||||
: -0
|
||||
) ?? []
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TabView
|
||||
pages={[
|
||||
{
|
||||
header: TabViewTranslate.deptAssignmentTable,
|
||||
element: (
|
||||
<DeptAssignmentTable
|
||||
submitNewDeptAssignment={(data) =>
|
||||
props.submitNewDeptAssignment(data)
|
||||
}
|
||||
submitDeptAssignmentUpdate={
|
||||
props.submitDeptAssignmentUpdate
|
||||
}
|
||||
assignments={activeAssignments()}
|
||||
submitStatus={props.submitState}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: TabViewTranslate.basicDeptAssignmentTable,
|
||||
element: (
|
||||
<BasicDeptAssignmentTable
|
||||
assignments={inActiveAssignments()}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: TabViewTranslate.deptAssignmentAduitTable,
|
||||
element: (
|
||||
<DeptAssignmentAuditTable
|
||||
assignments={props.employeeDepartmentAssignments}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeptAssignmentTabGroup;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { Badge } from "reactstrap";
|
||||
|
||||
export interface IHomeIconProps {
|
||||
isHome?: boolean;
|
||||
}
|
||||
|
||||
const HomeIcon: React.FC<IHomeIconProps> = (props) => {
|
||||
return (
|
||||
<Badge color="dark" hidden={!props.isHome}>
|
||||
<div className="d-flex flex-row justify-content-center align-items-center">
|
||||
<div>
|
||||
<p className="fa fa-home p-0 m-0" />
|
||||
</div>
|
||||
<b>
|
||||
<p className="mb-0 ml-2">Home</p>
|
||||
<p className="mb-0 ml-2">Dept</p>
|
||||
</b>
|
||||
</div>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeIcon;
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import React from "react";
|
||||
import moment from "moment";
|
||||
import { UserType } from "../../../../shared/models/Auditable";
|
||||
import { mount } from "enzyme";
|
||||
import DeptAssignmentEdit from "./DeptAssignmentEdit";
|
||||
import {
|
||||
IDepartmentAssignment,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../../../../shared/models/DepartmentAssignment";
|
||||
import { mockRoles } from "../../../../shared/testing/mocks/MockRoles";
|
||||
|
||||
const mockDeptAssignment: IDepartmentAssignment = {
|
||||
id: 5,
|
||||
employeeId: "5",
|
||||
departmentId: 55,
|
||||
startDate: moment("2021-01-02"),
|
||||
endDate: moment("2021-01-03"),
|
||||
note: "Note",
|
||||
isHome: false,
|
||||
createdByType: UserType.Associate,
|
||||
createdBy: "me",
|
||||
created: moment("2021-01-04"),
|
||||
lastUpdated: moment("2021-01-04"),
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
lastUpdatedBy: "me",
|
||||
role: {
|
||||
name: "Teller",
|
||||
description: "Teller Desc",
|
||||
hasCommissions: true,
|
||||
permissions: []
|
||||
},
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const mockGetDeptNameFromId = (id: any) => {
|
||||
return "WAUWATOSA";
|
||||
};
|
||||
const mockDisableEdit = jest.fn();
|
||||
const mockUpdateFormState = jest.fn();
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockDisableEdit.mockClear();
|
||||
mockUpdateFormState.mockClear();
|
||||
mockSubmit.mockClear();
|
||||
});
|
||||
|
||||
describe("A appUser wants to edit an employee's department assignments", () => {
|
||||
it("displays the expected data", () => {
|
||||
const mockState: IDepartmentAssignmentUpdateDto = {
|
||||
endDate: moment("2021-01-03"),
|
||||
role: "Teller",
|
||||
employeeId: "11771",
|
||||
assignmentId: 99,
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentEdit
|
||||
departmentAssignment={mockDeptAssignment}
|
||||
formState={mockState}
|
||||
getDepartmentNameFromId={mockGetDeptNameFromId}
|
||||
submitDeptAssignmentUpdate={() => mockSubmit()}
|
||||
updateFormState={() => mockUpdateFormState()}
|
||||
roles={mockRoles}
|
||||
disableEdit={() => mockDisableEdit()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="department"]').text()).toContain(
|
||||
"WAUWATOSA"
|
||||
);
|
||||
expect(wrapper.find('[data-test="note"]').text()).toContain("Note");
|
||||
expect(wrapper.find('[data-test="start-date"]').text()).toContain(
|
||||
"01-02-2021"
|
||||
);
|
||||
const endInput = wrapper.find('[data-test="end-date-input"]');
|
||||
// expect( endInput.first().html()).toEqual("");
|
||||
const endInputValue = endInput.first().prop("value");
|
||||
expect(endInputValue).toEqual("2021-01-03");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("handles null end date", () => {
|
||||
const mockState: IDepartmentAssignmentUpdateDto = {
|
||||
endDate: undefined,
|
||||
role: "Teller",
|
||||
employeeId: "11771",
|
||||
assignmentId: 99,
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentEdit
|
||||
departmentAssignment={mockDeptAssignment}
|
||||
formState={mockState}
|
||||
getDepartmentNameFromId={mockGetDeptNameFromId}
|
||||
submitDeptAssignmentUpdate={() => mockSubmit()}
|
||||
updateFormState={() => mockUpdateFormState()}
|
||||
roles={mockRoles}
|
||||
disableEdit={() => mockDisableEdit()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="department"]').text()).toContain(
|
||||
"WAUWATOSA"
|
||||
);
|
||||
expect(wrapper.find('[data-test="note"]').text()).toContain("Note");
|
||||
expect(wrapper.find('[data-test="start-date"]').text()).toContain(
|
||||
"01-02-2021"
|
||||
);
|
||||
const endInput = wrapper.find('[data-test="end-date-input"]');
|
||||
const endInputValue = endInput.first().prop("value");
|
||||
expect(endInputValue).toEqual("");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("A appUser wants to submit an edit to an employee's department assignments", () => {
|
||||
it("disables submit and displays feedback when end date is invalid - end is before start", () => {
|
||||
const mockState: IDepartmentAssignmentUpdateDto = {
|
||||
endDate: moment("2021-01-01"),
|
||||
role: "Teller",
|
||||
employeeId: "11771",
|
||||
assignmentId: 99,
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentEdit
|
||||
departmentAssignment={mockDeptAssignment}
|
||||
formState={mockState}
|
||||
getDepartmentNameFromId={mockGetDeptNameFromId}
|
||||
submitDeptAssignmentUpdate={() => mockSubmit()}
|
||||
updateFormState={() => mockUpdateFormState()}
|
||||
roles={mockRoles}
|
||||
disableEdit={() => mockDisableEdit()}
|
||||
/>
|
||||
);
|
||||
|
||||
const endInput = wrapper.find('[data-test="end-date-input"]');
|
||||
const endInputValue = endInput.first().prop("value");
|
||||
expect(endInputValue).toEqual("2021-01-01");
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(true);
|
||||
//Invalid and therefore displays feedback message, we trust reactstrap
|
||||
expect(endInput.first().prop("invalid")).toBe(true);
|
||||
});
|
||||
|
||||
it("calls submit with proper data when submit button is clicked", () => {
|
||||
const mockState: IDepartmentAssignmentUpdateDto = {
|
||||
endDate: moment("2021-01-03"),
|
||||
role: "Teller",
|
||||
employeeId: "11771",
|
||||
assignmentId: 99,
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentEdit
|
||||
departmentAssignment={mockDeptAssignment}
|
||||
formState={mockState}
|
||||
getDepartmentNameFromId={mockGetDeptNameFromId}
|
||||
submitDeptAssignmentUpdate={(form) => mockSubmit(form)}
|
||||
updateFormState={() => mockUpdateFormState()}
|
||||
roles={mockRoles}
|
||||
disableEdit={() => mockDisableEdit()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.prop("disabled")
|
||||
).toBe(false);
|
||||
wrapper
|
||||
.find('[data-test="submit-button"]')
|
||||
.hostNodes()
|
||||
.simulate("click");
|
||||
wrapper.update();
|
||||
expect(mockSubmit).toBeCalledTimes(1);
|
||||
expect(mockDisableEdit).toBeCalledTimes(1);
|
||||
expect(mockSubmit.mock.calls).toEqual([[mockState]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("A appUser wants to cancel an edit to an employee's department assignments", () => {
|
||||
it("does not submit and toggles edit to false", () => {
|
||||
const mockState: IDepartmentAssignmentUpdateDto = {
|
||||
endDate: moment("2021-01-03"),
|
||||
role: "Teller",
|
||||
employeeId: "11771",
|
||||
assignmentId: 99,
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentEdit
|
||||
departmentAssignment={mockDeptAssignment}
|
||||
formState={mockState}
|
||||
getDepartmentNameFromId={mockGetDeptNameFromId}
|
||||
submitDeptAssignmentUpdate={(form) => mockSubmit(form)}
|
||||
updateFormState={() => mockUpdateFormState()}
|
||||
roles={mockRoles}
|
||||
disableEdit={() => mockDisableEdit()}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test="clear-button"]')
|
||||
.hostNodes()
|
||||
.simulate("click");
|
||||
wrapper.update();
|
||||
expect(mockSubmit).toBeCalledTimes(0);
|
||||
expect(mockDisableEdit).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup, FormFeedback, Input } from "reactstrap";
|
||||
import { tryUpdateStateWithInput } from "../../../../shared/services/InputService";
|
||||
import moment, { Moment } from "moment";
|
||||
import HomeIcon from "../HomeIcon";
|
||||
import Translate from "./../../../../translate/Translate";
|
||||
import {
|
||||
IDepartmentAssignment,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../../../../shared/models/DepartmentAssignment";
|
||||
import { IAppRole } from "../../../../shared/models/Settings";
|
||||
|
||||
const newDepartmentAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTable
|
||||
.newDeptAssignmentRow;
|
||||
|
||||
export interface IDeptAssignmentTableRowEditProps {
|
||||
departmentAssignment: IDepartmentAssignment;
|
||||
formState: IDepartmentAssignmentUpdateDto;
|
||||
getDepartmentNameFromId: (deptId: number) => string;
|
||||
submitDeptAssignmentUpdate: (
|
||||
update: IDepartmentAssignmentUpdateDto
|
||||
) => void;
|
||||
disableEdit: () => void;
|
||||
updateFormState: (newState: IDepartmentAssignmentUpdateDto) => void;
|
||||
roles: IAppRole[];
|
||||
}
|
||||
|
||||
const DeptAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTable
|
||||
.deptAssignmentTableEdit;
|
||||
|
||||
const DeptAssignmentEdit: React.FC<IDeptAssignmentTableRowEditProps> = (
|
||||
props
|
||||
) => {
|
||||
const castMomentToStringOrPlaceholder = (
|
||||
value: Moment | undefined,
|
||||
fallBack: string
|
||||
): string => {
|
||||
if (value === undefined || value === null || !moment(value).isValid()) {
|
||||
return fallBack;
|
||||
}
|
||||
return moment(value).format("MM-DD-YYYY").toString();
|
||||
};
|
||||
|
||||
const isEndDateInvalid = () => {
|
||||
if (props.formState.endDate && props.departmentAssignment.startDate) {
|
||||
return moment(props.formState.endDate).isBefore(
|
||||
props.departmentAssignment.startDate
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
props.submitDeptAssignmentUpdate(props.formState);
|
||||
props.disableEdit();
|
||||
};
|
||||
|
||||
const isConfirmButtonDisabled = () => {
|
||||
return isEndDateInvalid();
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<HomeIcon isHome={props.departmentAssignment.isHome} />
|
||||
</td>
|
||||
<td data-test="department">
|
||||
{props.getDepartmentNameFromId(
|
||||
props.departmentAssignment.departmentId
|
||||
)}
|
||||
</td>
|
||||
<td data-test="start-date">
|
||||
{castMomentToStringOrPlaceholder(
|
||||
props.departmentAssignment.startDate,
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
data-test="end-date-input"
|
||||
disabled={props.departmentAssignment.isHome}
|
||||
value={
|
||||
props.formState.endDate
|
||||
?.format("YYYY-MM-DD")
|
||||
.toString() ?? ""
|
||||
}
|
||||
invalid={isEndDateInvalid()}
|
||||
onChange={(event) => {
|
||||
props.updateFormState(
|
||||
tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
"endDate",
|
||||
// FORCE to utc. Prevents a timezone conversion
|
||||
// you don't normally have to do this.
|
||||
moment.utc(event.target.value).endOf("month")
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
{DeptAssignmentTranslate.validation.endDate}
|
||||
</FormFeedback>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="role-input"
|
||||
disabled={props.departmentAssignment.isHome}
|
||||
value={
|
||||
props.formState.role === undefined
|
||||
? newDepartmentAssignmentTranslate.rolePlaceholder
|
||||
: props.formState.role
|
||||
}
|
||||
onChange={(event) => {
|
||||
props.updateFormState(
|
||||
tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
"role",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-role-option"
|
||||
hidden={props.formState.role !== undefined}
|
||||
key={undefined}
|
||||
>
|
||||
{newDepartmentAssignmentTranslate.rolePlaceholder}
|
||||
</option>
|
||||
{props.roles.map((role) => {
|
||||
return (
|
||||
<option key={role.name} value={role.name}>
|
||||
{role.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
type="number"
|
||||
max={100}
|
||||
min={0}
|
||||
bsSize="sm"
|
||||
data-test="sliding-payout-input"
|
||||
maxLength={3}
|
||||
value={props.formState.slidingPayout ?? ""}
|
||||
onChange={(event) => {
|
||||
props.updateFormState(
|
||||
tryUpdateStateWithInput(
|
||||
props.formState,
|
||||
"slidingPayout",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td data-test="note">{props.departmentAssignment.note}</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="danger"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
submitForm();
|
||||
}}
|
||||
data-test="submit-button"
|
||||
disabled={isConfirmButtonDisabled()}
|
||||
>
|
||||
{DeptAssignmentTranslate.submitButton}
|
||||
</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 DeptAssignmentEdit;
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
import { shallow } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import DeptAssignmentNew from "./DeptAssignmentNew";
|
||||
import { mockRoles } from "../../../../shared/testing/mocks/MockRoles";
|
||||
import { MultiSelectOption } from "../../../../shared/types/IMultiSelectOption";
|
||||
|
||||
const mockDepartments = [
|
||||
{
|
||||
departmentId: 0,
|
||||
id: 55,
|
||||
departmentName: "WAUWATOSA",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
departmentId: 1,
|
||||
id: 56,
|
||||
departmentName: "HEADQUARTERS",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active"
|
||||
}
|
||||
];
|
||||
|
||||
const departmentOptions = mockDepartments.map(
|
||||
(department) =>
|
||||
({
|
||||
key: department.id?.toString(),
|
||||
label: department.departmentName,
|
||||
value: department.id?.toString()
|
||||
} as MultiSelectOption)
|
||||
);
|
||||
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockSubmit.mockClear();
|
||||
});
|
||||
|
||||
describe("User wants to add a new dept assignment", () => {
|
||||
it("updates state when input changed", () => {
|
||||
const wrapper = shallow(
|
||||
<DeptAssignmentNew
|
||||
roles={mockRoles}
|
||||
departments={mockDepartments}
|
||||
submitNewDepartmentAssignment={(data) => mockSubmit(data)}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="dept-input"]').prop("value")).toEqual(
|
||||
[]
|
||||
);
|
||||
expect(wrapper.find('[data-test="note-input"]').prop("value")).toBe("");
|
||||
expect(wrapper.find('[data-test="start-input"]').prop("value")).toEqual(
|
||||
""
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("value")).toEqual(
|
||||
""
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("invalid")).toBe(
|
||||
false
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test="dept-input"]')
|
||||
.simulate("change", { target: { value: [departmentOptions[0]] } });
|
||||
wrapper
|
||||
.find('[data-test="note-input"]')
|
||||
.simulate("change", { target: { value: "note" } });
|
||||
wrapper
|
||||
.find('[data-test="start-input"]')
|
||||
.simulate("change", { target: { value: moment("2021-01-01") } });
|
||||
wrapper
|
||||
.find('[data-test="end-input"]')
|
||||
.simulate("change", { target: { value: moment("2021-01-02") } });
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test="dept-input"]').prop("value")).toEqual({
|
||||
target: { value: [departmentOptions[0]] }
|
||||
});
|
||||
expect(wrapper.find('[data-test="note-input"]').prop("value")).toBe(
|
||||
"note"
|
||||
);
|
||||
expect(wrapper.find('[data-test="start-input"]').prop("value")).toEqual(
|
||||
"2021-01-01"
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("value")).toEqual(
|
||||
"2021-01-31"
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("invalid")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
it("disables submit until required data is entered", () => {
|
||||
const wrapper = shallow(
|
||||
<DeptAssignmentNew
|
||||
roles={mockRoles}
|
||||
departments={mockDepartments}
|
||||
submitNewDepartmentAssignment={(data) => mockSubmit(data)}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-input"]')
|
||||
.simulate("change", { target: { value: [departmentOptions[0]] } });
|
||||
//Note not required
|
||||
wrapper
|
||||
.find('[data-test="start-input"]')
|
||||
.simulate("change", { target: { value: moment("2021-01-01") } });
|
||||
//EndDate Not Required
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').prop("disabled")
|
||||
).toBe(false);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("invalid")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
it("disables submit if endDate is before startDate", () => {
|
||||
const wrapper = shallow(
|
||||
<DeptAssignmentNew
|
||||
roles={mockRoles}
|
||||
departments={mockDepartments}
|
||||
submitNewDepartmentAssignment={(data) => mockSubmit(data)}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').prop("disabled")
|
||||
).toBe(true);
|
||||
wrapper
|
||||
.find('[data-test="dept-input"]')
|
||||
.simulate("change", { target: { value: [departmentOptions[0]] } });
|
||||
wrapper
|
||||
.find('[data-test="note-input"]')
|
||||
.simulate("change", { target: { value: "note" } });
|
||||
wrapper
|
||||
.find('[data-test="start-input"]')
|
||||
.simulate("change", { target: { value: moment("2021-02-01") } });
|
||||
wrapper
|
||||
.find('[data-test="end-input"]')
|
||||
.simulate("change", { target: { value: moment("2021-01-02") } });
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("invalid")).toBe(
|
||||
true
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').prop("disabled")
|
||||
).toBe(true);
|
||||
});
|
||||
it("hides the clear button when there is nothing to clear", () => {
|
||||
const wrapper = shallow(
|
||||
<DeptAssignmentNew
|
||||
roles={mockRoles}
|
||||
departments={mockDepartments}
|
||||
submitNewDepartmentAssignment={(data) => mockSubmit(data)}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="clear-button"]').prop("hidden")).toBe(
|
||||
true
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test="dept-input"]')
|
||||
.simulate("change", { target: { value: [departmentOptions[0]] } });
|
||||
wrapper
|
||||
.find('[data-test="note-input"]')
|
||||
.simulate("change", { target: { value: "note" } });
|
||||
expect(wrapper.find('[data-test="clear-button"]').prop("hidden")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User no longer wishes to add a new dept assignment", () => {
|
||||
it("resets all inputs when the clear button is clicked", () => {
|
||||
const wrapper = shallow(
|
||||
<DeptAssignmentNew
|
||||
roles={mockRoles}
|
||||
departments={mockDepartments}
|
||||
submitNewDepartmentAssignment={(data) => mockSubmit(data)}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="clear-button"]').prop("hidden")).toBe(
|
||||
true
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test="dept-input"]')
|
||||
.simulate("change", { target: { value: [departmentOptions[0]] } });
|
||||
wrapper
|
||||
.find('[data-test="note-input"]')
|
||||
.simulate("change", { target: { value: "note" } });
|
||||
expect(wrapper.find('[data-test="clear-button"]').prop("hidden")).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.find('[data-test="clear-button"]').simulate("click");
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test="dept-input"]').prop("value")).toEqual(
|
||||
[]
|
||||
);
|
||||
expect(wrapper.find('[data-test="note-input"]').prop("value")).toBe("");
|
||||
expect(wrapper.find('[data-test="start-input"]').prop("value")).toEqual(
|
||||
""
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("value")).toEqual(
|
||||
""
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("invalid")).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User submits a new dept assignment", () => {
|
||||
it("passes the proper data on submit", () => {
|
||||
const wrapper = shallow(
|
||||
<DeptAssignmentNew
|
||||
roles={mockRoles}
|
||||
departments={mockDepartments}
|
||||
submitNewDepartmentAssignment={(data) => mockSubmit(data)}
|
||||
/>
|
||||
);
|
||||
wrapper
|
||||
.find('[data-test="dept-input"]')
|
||||
.simulate("change", { target: { value: [departmentOptions[0]] } });
|
||||
wrapper
|
||||
.find('[data-test="role-input"]')
|
||||
.simulate("change", { target: { value: "Admin" } });
|
||||
wrapper
|
||||
.find('[data-test="note-input"]')
|
||||
.simulate("change", { target: { value: "note" } });
|
||||
wrapper
|
||||
.find('[data-test="sliding-payout-input"]')
|
||||
.simulate("change", { target: { value: "65" } });
|
||||
wrapper
|
||||
.find('[data-test="start-input"]')
|
||||
.simulate("change", { target: { value: "2021-01-01" } });
|
||||
wrapper
|
||||
.find('[data-test="end-input"]')
|
||||
.simulate("change", { target: { value: "2021-01-02" } });
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('[data-test="submit-button"]').prop("disabled")
|
||||
).toBe(false);
|
||||
wrapper.find('[data-test="submit-button"]').simulate("click");
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test="dept-input"]').prop("value")).toEqual(
|
||||
[]
|
||||
);
|
||||
expect(wrapper.find('[data-test="note-input"]').prop("value")).toBe("");
|
||||
expect(wrapper.find('[data-test="start-input"]').prop("value")).toEqual(
|
||||
""
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("value")).toEqual(
|
||||
""
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-input"]').prop("invalid")).toBe(
|
||||
false
|
||||
);
|
||||
//start and end date should be cast as UTC!
|
||||
});
|
||||
});
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
import React, { useState } from "react";
|
||||
import Translate from "../../../../translate/Translate";
|
||||
import { IDepartment } from "../../../../shared/models/Department";
|
||||
import { Button, ButtonGroup, FormFeedback, Input } from "reactstrap";
|
||||
import { tryUpdateStateWithInput } from "../../../../shared/services/InputService";
|
||||
import moment from "moment";
|
||||
import { IDepartmentAssignmentDto } from "../../../../shared/models/DepartmentAssignment";
|
||||
import { IAppRole } from "../../../../shared/models/Settings";
|
||||
import MultiSelect from "react-multi-select-component";
|
||||
import { MultiSelectOption } from "../../../../shared/types/IMultiSelectOption";
|
||||
|
||||
export interface INewDeptAssignmentRowProps {
|
||||
departments: IDepartment[];
|
||||
roles: IAppRole[];
|
||||
submitNewDepartmentAssignment: (
|
||||
newDepartmentData: Partial<IDepartmentAssignmentDto>
|
||||
) => void;
|
||||
}
|
||||
|
||||
const newDepartmentAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTable
|
||||
.newDeptAssignmentRow;
|
||||
|
||||
const DeptAssignmentNew: React.FC<INewDeptAssignmentRowProps> = (props) => {
|
||||
const emptyFormState = {
|
||||
departmentId: [],
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
note: undefined,
|
||||
role: undefined,
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
const isSubmitDisabled = (): boolean => {
|
||||
return (
|
||||
selectedNewDepartmentIds?.length === 0 ||
|
||||
formState.startDate === undefined ||
|
||||
!formState.startDate.isValid() ||
|
||||
isEndDateInvalid()
|
||||
);
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
props.submitNewDepartmentAssignment(formState);
|
||||
clearFormState();
|
||||
};
|
||||
|
||||
const clearFormState = () => {
|
||||
setFormState(emptyFormState);
|
||||
setSelectedNewDepartmentIds([]);
|
||||
};
|
||||
|
||||
const shouldHideClear = () => {
|
||||
return (
|
||||
selectedNewDepartmentIds?.length === 0 &&
|
||||
formState.startDate === undefined &&
|
||||
formState.endDate === undefined &&
|
||||
formState.note === undefined
|
||||
);
|
||||
};
|
||||
|
||||
const isEndDateInvalid = () => {
|
||||
if (formState.endDate && formState.startDate) {
|
||||
return moment(formState.endDate).isBefore(formState.startDate);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const [formState, setFormState] = useState<
|
||||
Partial<IDepartmentAssignmentDto>
|
||||
>(emptyFormState);
|
||||
|
||||
const departmentOptions = props.departments
|
||||
.filter((x) => x.state === "Active")
|
||||
.map(
|
||||
(department) =>
|
||||
({
|
||||
key: department.id?.toString(),
|
||||
label: department.departmentName,
|
||||
value: department.id?.toString()
|
||||
} as MultiSelectOption)
|
||||
);
|
||||
|
||||
const [selectedNewDepartmentIds, setSelectedNewDepartmentIds] = useState<
|
||||
MultiSelectOption[]
|
||||
>([]);
|
||||
|
||||
const handleSelected = (options: MultiSelectOption[]) => {
|
||||
formState.departmentId = Array.from(
|
||||
options
|
||||
).map((e: MultiSelectOption) => parseInt(e.value, 10));
|
||||
setSelectedNewDepartmentIds(options);
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td />
|
||||
<td>
|
||||
<MultiSelect
|
||||
data-test="dept-input"
|
||||
options={departmentOptions}
|
||||
value={selectedNewDepartmentIds}
|
||||
onChange={handleSelected}
|
||||
labelledBy="Select"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
data-test="start-input"
|
||||
value={
|
||||
formState.startDate?.format("YYYY-MM-DD").toString() ??
|
||||
""
|
||||
}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
formState,
|
||||
"startDate",
|
||||
// FORCE to utc. Prevents a timezone conversion
|
||||
// you don't normally have to do this.
|
||||
moment.utc(event.target.value).startOf("month")
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
type="date"
|
||||
bsSize="sm"
|
||||
value={
|
||||
formState.endDate?.format("YYYY-MM-DD").toString() ?? ""
|
||||
}
|
||||
data-test="end-input"
|
||||
invalid={isEndDateInvalid()}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
formState,
|
||||
"endDate",
|
||||
// FORCE to utc. Prevents a timezone conversion
|
||||
// you don't normally have to do this.
|
||||
moment.utc(event.target.value).endOf("month")
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<FormFeedback>
|
||||
{
|
||||
newDepartmentAssignmentTranslate.endDate.validation
|
||||
.invalid
|
||||
}
|
||||
</FormFeedback>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="select"
|
||||
data-test="role-input"
|
||||
value={
|
||||
formState.role === undefined
|
||||
? newDepartmentAssignmentTranslate.rolePlaceholder
|
||||
: formState.role
|
||||
}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
formState,
|
||||
"role",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option
|
||||
data-test="placeholder-role-option"
|
||||
hidden={formState.role !== undefined}
|
||||
key={undefined}
|
||||
>
|
||||
{newDepartmentAssignmentTranslate.rolePlaceholder}
|
||||
</option>
|
||||
{props.roles.map((role) => {
|
||||
return (
|
||||
<option key={role.name} value={role.name}>
|
||||
{role.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Input>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
type="number"
|
||||
max={100}
|
||||
min={0}
|
||||
bsSize="sm"
|
||||
data-test="sliding-payout-input"
|
||||
maxLength={3}
|
||||
value={formState.slidingPayout ?? ""}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
formState,
|
||||
"slidingPayout",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
data-test="note-input"
|
||||
maxLength={255}
|
||||
placeholder={
|
||||
newDepartmentAssignmentTranslate.notePlaceholder
|
||||
}
|
||||
value={formState.note ?? ""}
|
||||
onChange={(event) => {
|
||||
setFormState(
|
||||
tryUpdateStateWithInput(
|
||||
formState,
|
||||
"note",
|
||||
event.target.value
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
data-test="submit-button"
|
||||
disabled={isSubmitDisabled()}
|
||||
onClick={() => {
|
||||
submitForm();
|
||||
}}
|
||||
>
|
||||
{newDepartmentAssignmentTranslate.submitButton}
|
||||
</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 DeptAssignmentNew;
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { mount } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IProfileState } from "../../../../reducers/ProfileReducer";
|
||||
import AsyncState from "../../../../shared/enumerations/AsyncState";
|
||||
import PermissionCode from "../../../../shared/enumerations/PermissionCode";
|
||||
import { UserType } from "../../../../shared/models/Auditable";
|
||||
import Translate from "../../../../translate/Translate";
|
||||
import DeptAssignmentRow from "./DeptAssignmentRow";
|
||||
import { IDepartmentAssignment } from "../../../../shared/models/DepartmentAssignment";
|
||||
import { mockRoles } from "../../../../shared/testing/mocks/MockRoles";
|
||||
|
||||
export interface IFakePermissionProps {
|
||||
requiredPermissions: string[];
|
||||
showWarning?: boolean;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const FakePermissionGuard: React.FC<IFakePermissionProps> = (props) => {
|
||||
return props.children;
|
||||
};
|
||||
|
||||
jest.mock("../../../shared/PermissionGuard", () => FakePermissionGuard);
|
||||
|
||||
const departments = [
|
||||
{
|
||||
departmentId: 5,
|
||||
id: 55,
|
||||
departmentName: "WAUWATOSA",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
departmentId: 1,
|
||||
id: 56,
|
||||
departmentName: "HEADQUARTERS",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active"
|
||||
}
|
||||
];
|
||||
|
||||
const mockDeptAssignment: IDepartmentAssignment = {
|
||||
id: 5,
|
||||
employeeId: "5",
|
||||
departmentId: 55,
|
||||
startDate: moment("2021-01-02"),
|
||||
endDate: moment("2021-01-03"),
|
||||
note: "Note",
|
||||
isHome: false,
|
||||
createdByType: UserType.Associate,
|
||||
createdBy: "me",
|
||||
created: moment("2021-01-04"),
|
||||
lastUpdated: moment("2021-01-04"),
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
lastUpdatedBy: "me",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT,
|
||||
description: "ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
slidingPayout: 100
|
||||
};
|
||||
|
||||
// setup appUser to get past permission guard
|
||||
const mockProfileState: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
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: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
|
||||
describe("An Associate Logs in and views the DeptAssignmentTable", () => {
|
||||
it("Should display expected data", () => {
|
||||
useSelectorMock.mockReturnValue(mockProfileState);
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentRow
|
||||
roles={mockRoles}
|
||||
departments={departments}
|
||||
departmentAssignment={mockDeptAssignment}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="department"]').text()).toContain(
|
||||
"WAUWATOSA"
|
||||
);
|
||||
expect(wrapper.find('[data-test="note"]').text()).toContain("Note");
|
||||
expect(wrapper.find('[data-test="start-date"]').text()).toContain(
|
||||
"01-02-2021"
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-date"]').text()).toContain(
|
||||
"01-03-2021"
|
||||
);
|
||||
});
|
||||
|
||||
it("handles missing deparment", () => {
|
||||
useSelectorMock.mockReturnValue(mockProfileState);
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentRow
|
||||
roles={mockRoles}
|
||||
departments={departments}
|
||||
departmentAssignment={{
|
||||
...mockDeptAssignment,
|
||||
departmentId: 0
|
||||
}}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="department"]').text()).toContain("-");
|
||||
expect(wrapper.find('[data-test="note"]').text()).toContain("Note");
|
||||
expect(wrapper.find('[data-test="start-date"]').text()).toContain(
|
||||
"01-02-2021"
|
||||
);
|
||||
expect(wrapper.find('[data-test="end-date"]').text()).toContain(
|
||||
"01-03-2021"
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults missing start/end date", () => {
|
||||
useSelectorMock.mockReturnValue(mockProfileState);
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentRow
|
||||
roles={mockRoles}
|
||||
departments={departments}
|
||||
/* @ts-ignore */
|
||||
departmentAssignment={{
|
||||
...mockDeptAssignment,
|
||||
startDate: moment(),
|
||||
endDate: undefined
|
||||
}}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test="department"]').text()).toContain(
|
||||
"WAUWATOSA"
|
||||
);
|
||||
expect(wrapper.find('[data-test="note"]').text()).toContain("Note");
|
||||
expect(wrapper.find('[data-test="start-date"]').text()).toContain("-");
|
||||
expect(wrapper.find('[data-test="end-date"]').text()).toContain(
|
||||
Translate.Get.containers.employeePage.deptAssignment
|
||||
.deptAssignmentTable.deptAssignmentTableRow.noEndDate
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("An Associate Logs in and wants to update the end date of a DeptAssignmentTable", () => {
|
||||
it("Toggling Edit Displays Edit Row", () => {
|
||||
useSelectorMock.mockReturnValue(mockProfileState);
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentRow
|
||||
roles={mockRoles}
|
||||
departments={departments}
|
||||
/* @ts-ignore */
|
||||
departmentAssignment={{
|
||||
...mockDeptAssignment,
|
||||
endDate: undefined
|
||||
}}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(0);
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('[data-test="edit-row"]').prop("formState")
|
||||
).toEqual({
|
||||
assignmentId: 5,
|
||||
employeeId: "5",
|
||||
endDate: undefined,
|
||||
role: "Admin",
|
||||
slidingPayout: 100
|
||||
});
|
||||
});
|
||||
|
||||
it("Toggling Edit for a row with Data sets formState to include the existing end date", () => {
|
||||
useSelectorMock.mockReturnValue(mockProfileState);
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentRow
|
||||
roles={mockRoles}
|
||||
departments={departments}
|
||||
departmentAssignment={{
|
||||
...mockDeptAssignment,
|
||||
endDate: moment("2021-02-01")
|
||||
}}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(0);
|
||||
wrapper.find("Button").simulate("click");
|
||||
wrapper.update();
|
||||
expect(wrapper.find('[data-test="edit-row"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('[data-test="edit-row"]').prop("formState")
|
||||
).toEqual({
|
||||
assignmentId: 5,
|
||||
employeeId: "5",
|
||||
endDate: moment("2021-02-01"),
|
||||
role: "Admin",
|
||||
slidingPayout: 100
|
||||
});
|
||||
});
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import React, { useState } from "react";
|
||||
import { IDepartment } from "../../../../shared/models/Department";
|
||||
import moment, { Moment } from "moment";
|
||||
import DeptAssignmentEdit from "./DeptAssignmentEdit";
|
||||
import { Button } from "reactstrap";
|
||||
import HomeIcon from "../HomeIcon";
|
||||
import Translate from "./../../../../translate/Translate";
|
||||
import PermissionGuard from "../../../shared/PermissionGuard";
|
||||
import PermissionCode from "../../../../shared/enumerations/PermissionCode";
|
||||
import {
|
||||
IDepartmentAssignment,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../../../../shared/models/DepartmentAssignment";
|
||||
import { IAppRole } from "../../../../shared/models/Settings";
|
||||
|
||||
export interface IDeptAssignmentTableRowProps {
|
||||
departments: IDepartment[];
|
||||
departmentAssignment: IDepartmentAssignment;
|
||||
submitDeptAssignmentUpdate: (
|
||||
update: IDepartmentAssignmentUpdateDto
|
||||
) => void;
|
||||
roles: IAppRole[];
|
||||
}
|
||||
|
||||
const DeptAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTable
|
||||
.deptAssignmentTableRow;
|
||||
|
||||
const DeptAssignmentRow: React.FC<IDeptAssignmentTableRowProps> = (props) => {
|
||||
const createFormStateFromAssignment = (
|
||||
departmentAssignment: IDepartmentAssignment
|
||||
): IDepartmentAssignmentUpdateDto => {
|
||||
const dto = {
|
||||
assignmentId: departmentAssignment.id!,
|
||||
employeeId: departmentAssignment.employeeId,
|
||||
endDate: departmentAssignment.endDate
|
||||
? moment(departmentAssignment.endDate)
|
||||
: undefined,
|
||||
role: departmentAssignment.role?.name || "MISSING",
|
||||
slidingPayout: departmentAssignment.slidingPayout
|
||||
};
|
||||
return dto;
|
||||
};
|
||||
|
||||
const initialFormState = createFormStateFromAssignment(
|
||||
props.departmentAssignment
|
||||
);
|
||||
const [isEditing, setEdit] = useState<boolean>(false);
|
||||
const [formState, setFormState] = useState<IDepartmentAssignmentUpdateDto>(
|
||||
initialFormState
|
||||
);
|
||||
|
||||
const enableEdit = () => {
|
||||
setFormState(createFormStateFromAssignment(props.departmentAssignment));
|
||||
setEdit(true);
|
||||
};
|
||||
|
||||
const getDepartmentNameFromId = (id: number): string => {
|
||||
return (
|
||||
props.departments.find((dept) => dept.id === id)?.departmentName ??
|
||||
"-"
|
||||
);
|
||||
};
|
||||
|
||||
const castMomentToStringOrPlaceholder = (
|
||||
value: Moment | undefined,
|
||||
fallBack: string
|
||||
): string => {
|
||||
if (value === undefined || value === null || !moment(value).isValid()) {
|
||||
return fallBack;
|
||||
}
|
||||
return moment(value).format("MM-DD-YYYY").toString();
|
||||
};
|
||||
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<HomeIcon isHome={props.departmentAssignment.isHome} />
|
||||
</td>
|
||||
<td data-test="department">
|
||||
{getDepartmentNameFromId(
|
||||
props.departmentAssignment.departmentId
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td data-test="start-date">
|
||||
{castMomentToStringOrPlaceholder(
|
||||
props.departmentAssignment.startDate,
|
||||
"-"
|
||||
)}
|
||||
</td>
|
||||
<td data-test="end-date">
|
||||
{castMomentToStringOrPlaceholder(
|
||||
props.departmentAssignment.endDate,
|
||||
DeptAssignmentTranslate.noEndDate
|
||||
)}
|
||||
</td>
|
||||
<td data-test="role">
|
||||
{props.departmentAssignment.role
|
||||
? props.departmentAssignment.role.name
|
||||
: "Missing!"}
|
||||
</td>
|
||||
<td data-test="payout">
|
||||
{props.departmentAssignment.slidingPayout}%
|
||||
</td>
|
||||
<td data-test="note">{props.departmentAssignment.note}</td>
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT
|
||||
]}
|
||||
>
|
||||
<td>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={() => enableEdit()}
|
||||
>
|
||||
{DeptAssignmentTranslate.editButton}
|
||||
</Button>
|
||||
</td>
|
||||
</PermissionGuard>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DeptAssignmentEdit
|
||||
formState={formState}
|
||||
roles={props.roles}
|
||||
departmentAssignment={props.departmentAssignment}
|
||||
getDepartmentNameFromId={(deptId: number) =>
|
||||
getDepartmentNameFromId(deptId)
|
||||
}
|
||||
submitDeptAssignmentUpdate={props.submitDeptAssignmentUpdate}
|
||||
disableEdit={() => setEdit(false)}
|
||||
updateFormState={(newState) => setFormState(newState)}
|
||||
data-test="edit-row"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeptAssignmentRow;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import DeptAssignmentTable from "./DeptAssignmentTable";
|
||||
import AsyncState from "./../../../../shared/enumerations/AsyncState";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IDepartmentState } from "../../../../reducers/DepartmentReducer";
|
||||
import { mockDeptAssignments } from "../../../../shared/testing/mocks/MockDepartmentAssignments";
|
||||
import { mockProfileState } from "../../../../shared/testing/mocks/MockProfileState";
|
||||
import { getMockAppSettings } from "../../../../shared/testing/mocks/MockAppSettings";
|
||||
|
||||
export interface IFakePermissionProps {
|
||||
requiredPermissions: string[];
|
||||
showWarning?: boolean;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export const FakePermissionGuard: React.FC<IFakePermissionProps> = (props) => {
|
||||
return props.children;
|
||||
};
|
||||
|
||||
jest.mock("../../../shared/PermissionGuard", () => FakePermissionGuard);
|
||||
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
|
||||
beforeEach(() => {
|
||||
useDispatchMock.mockClear();
|
||||
useSelectorMock.mockClear();
|
||||
useContextMock.mockClear();
|
||||
});
|
||||
|
||||
const mockDepartmentState: IDepartmentState = {
|
||||
departments: {
|
||||
data: [
|
||||
{
|
||||
departmentId: 0,
|
||||
id: 55,
|
||||
departmentName: "WAUWATOSA",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active"
|
||||
},
|
||||
{
|
||||
departmentId: 1,
|
||||
id: 56,
|
||||
departmentName: "HEADQUARTERS",
|
||||
businessLineCode: "OPS",
|
||||
state: "Active"
|
||||
}
|
||||
],
|
||||
state: AsyncState.Success
|
||||
},
|
||||
submitState: { state: AsyncState.Inactive }
|
||||
};
|
||||
|
||||
describe("Associate views Emoloyees Department Assignments", () => {
|
||||
it("Displays all available dept assignment table row", () => {
|
||||
useDispatchMock.mockReturnValue(jest.fn());
|
||||
useSelectorMock.mockReturnValue({
|
||||
...mockDepartmentState,
|
||||
...mockProfileState
|
||||
});
|
||||
useContextMock.mockReturnValue(getMockAppSettings());
|
||||
|
||||
const wrapper = mount(
|
||||
<DeptAssignmentTable
|
||||
assignments={mockDeptAssignments}
|
||||
submitStatus={{ state: AsyncState.Inactive }}
|
||||
submitDeptAssignmentUpdate={() => jest.fn()}
|
||||
submitNewDeptAssignment={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find("DeptAssignmentRow").length).toBe(4);
|
||||
expect(wrapper.find("DeptAssignmentNew").length).toBe(1);
|
||||
});
|
||||
});
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
import Translate from "../../../../translate/Translate";
|
||||
import DeptAssignmentRow from "./DeptAssignmentRow";
|
||||
import IAsyncStateContainer from "../../../../shared/types/IAsyncStateContainer";
|
||||
import DeptAssignmentAlertBar from "../DeptAssignmentAlertBar";
|
||||
import PermissionGuard from "../../../shared/PermissionGuard";
|
||||
import PermissionCode from "../../../../shared/enumerations/PermissionCode";
|
||||
import {
|
||||
IDepartmentAssignment,
|
||||
IDepartmentAssignmentDto,
|
||||
IDepartmentAssignmentUpdateDto
|
||||
} from "../../../../shared/models/DepartmentAssignment";
|
||||
import DeptAssignmentNew from "./DeptAssignmentNew";
|
||||
import { SettingsContext } from "../../../../shared/providers/SettingsProvider";
|
||||
import SettingsService from "../../../../shared/services/SettingsService";
|
||||
import { useSelector } from "react-redux";
|
||||
import { IAppState } from "../../../../store/Store";
|
||||
import { IProfileState } from "../../../../reducers/ProfileReducer";
|
||||
import ProfileService from "../../../../shared/services/ProfileService";
|
||||
|
||||
export interface IDeptAssignmentTableProps {
|
||||
assignments: IDepartmentAssignment[];
|
||||
submitStatus: IAsyncStateContainer<string>;
|
||||
submitNewDeptAssignment: (
|
||||
deptAssignment: Partial<IDepartmentAssignmentDto>
|
||||
) => void;
|
||||
submitDeptAssignmentUpdate: (
|
||||
deptUpdate: IDepartmentAssignmentUpdateDto
|
||||
) => void;
|
||||
}
|
||||
|
||||
const departmentAssignmentTranslate =
|
||||
Translate.Get.containers.employeePage.deptAssignment.deptAssignmentTable;
|
||||
|
||||
const DeptAssignmentTable: React.FC<IDeptAssignmentTableProps> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const profileState = useSelector<IAppState, IProfileState>(
|
||||
(state) => state.profileState
|
||||
);
|
||||
const profile = profileState.profile;
|
||||
|
||||
const maxRank = profile.data
|
||||
? ProfileService.findHighestRank(profile.data)
|
||||
: 1;
|
||||
const assignableRoles = SettingsService.getAssignableRoles(
|
||||
settings?.appRoles,
|
||||
maxRank
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<DeptAssignmentAlertBar submitState={props.submitStatus} />
|
||||
<div className="border-top-0">
|
||||
<table className="table table-striped border tablecenterCSS tablecenterheadCSS">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{departmentAssignmentTranslate.headers.isHome}
|
||||
</th>
|
||||
<th>
|
||||
{
|
||||
departmentAssignmentTranslate.headers
|
||||
.departmentId
|
||||
}
|
||||
</th>
|
||||
<th>
|
||||
{
|
||||
departmentAssignmentTranslate.headers
|
||||
.startDate
|
||||
}
|
||||
</th>
|
||||
<th>
|
||||
<i className="fa fa-long-arrow-down"> </i>
|
||||
{departmentAssignmentTranslate.headers.endDate}
|
||||
</th>
|
||||
<th>
|
||||
{departmentAssignmentTranslate.headers.role}
|
||||
</th>
|
||||
<th>
|
||||
{departmentAssignmentTranslate.headers.payout}
|
||||
</th>
|
||||
<th>
|
||||
{departmentAssignmentTranslate.headers.note}
|
||||
</th>
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT
|
||||
]}
|
||||
>
|
||||
<th />
|
||||
</PermissionGuard>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT
|
||||
]}
|
||||
>
|
||||
<DeptAssignmentNew
|
||||
departments={settings?.departments ?? []}
|
||||
roles={assignableRoles}
|
||||
submitNewDepartmentAssignment={(
|
||||
deptAssignment: Partial<IDepartmentAssignmentDto>
|
||||
) => {
|
||||
props.submitNewDeptAssignment(
|
||||
deptAssignment
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PermissionGuard>
|
||||
{props.assignments.map((assignment) => {
|
||||
return (
|
||||
<DeptAssignmentRow
|
||||
roles={assignableRoles}
|
||||
key={
|
||||
assignment.id +
|
||||
assignment.lastUpdated.toString()
|
||||
}
|
||||
departments={settings?.departments ?? []}
|
||||
departmentAssignment={assignment}
|
||||
submitDeptAssignmentUpdate={
|
||||
props.submitDeptAssignmentUpdate
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default DeptAssignmentTable;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import React, { useState } from "react";
|
||||
import moment, { Moment } from "moment";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { Alert, Input } from "reactstrap";
|
||||
import PayoutService from "../../shared/services/PayoutService";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import PayoutEntryTable from "../../components/payouts/PayoutEntryTable";
|
||||
import ExportCsvService from "../../shared/services/ExportCsvService";
|
||||
import { IEmployeeSummary } from "../../shared/models/Employee";
|
||||
import EndOfEmploymentPayoutView from "../../components/payouts/EndOfEmploymentPayoutView";
|
||||
import TabView from "../../components/tab-view/TabView";
|
||||
import ProfileDao from "../../shared/dao/ProfileDao";
|
||||
|
||||
const EndOfEmploymentPage: React.FC = () => {
|
||||
const [date, setDate] = useState<Moment>(moment());
|
||||
const [payouts, setPayouts] = useState<
|
||||
IAsyncStateContainer<IEmployeeSummary[]>
|
||||
>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
|
||||
const submitForm = () => {
|
||||
fetchData().then((employeeSummaries) => {
|
||||
setPayouts(employeeSummaries);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
return await ProfileDao.GetEndOfEmploymentList(
|
||||
date.year(),
|
||||
date.month() + 1 // moment months are 0 index.
|
||||
);
|
||||
};
|
||||
|
||||
const exportCsv = () => {
|
||||
if (payouts.data && payouts.data.length > 0) {
|
||||
const converted = PayoutService.CreateEndOfEmploymentPayouts(
|
||||
payouts.data
|
||||
);
|
||||
ExportCsvService.exportDayforcePayoutCSV(converted);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test="export-payout-page">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<Alert color="info">
|
||||
{
|
||||
Translate.Get.containers.endOfEmploymentPage
|
||||
.instructions
|
||||
}
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-sm-6 col-md-2 col-lg-2">
|
||||
<Input
|
||||
name="AcceptedStateDateTime"
|
||||
type="month"
|
||||
bsSize="sm"
|
||||
required
|
||||
className="mb-1 ml-2 inline"
|
||||
value={date.format("YYYY-MM")}
|
||||
onChange={(event) => {
|
||||
setDate(
|
||||
moment(event.target.value).startOf("month")
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<button
|
||||
className="btn btn-outline-info btn-sm mx-2"
|
||||
type="button"
|
||||
onClick={submitForm}
|
||||
>
|
||||
{
|
||||
Translate.Get.containers.endOfEmploymentPage
|
||||
.exportButton
|
||||
}
|
||||
</button>
|
||||
<AsyncGuard container={payouts}>
|
||||
{payouts.data && payouts.data.length > 0 ? (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={exportCsv}
|
||||
>
|
||||
{" "}
|
||||
{
|
||||
Translate.Get.containers.endOfEmploymentPage
|
||||
.downloadButton
|
||||
}
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</AsyncGuard>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row mt-2">
|
||||
<div className="col">
|
||||
<AsyncGuard container={payouts}>
|
||||
<>
|
||||
<TabView
|
||||
data-test="progress-tabbed-view"
|
||||
pages={[
|
||||
{
|
||||
header: "Payouts",
|
||||
element: (
|
||||
<PayoutEntryTable
|
||||
payoutEntries={
|
||||
PayoutService.CreateEndOfEmploymentPayouts(
|
||||
payouts.data!
|
||||
) || []
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: "Details",
|
||||
element: (
|
||||
<EndOfEmploymentPayoutView
|
||||
eoeEmployees={payouts.data!}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
</AsyncGuard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndOfEmploymentPage;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import GoalEditCell from "./GoalEditCell";
|
||||
import { IGoalEditInput } from "../../shared/models/Goal";
|
||||
import { Product2 } from "../../shared/enumerations/Product";
|
||||
|
||||
describe("A cell for editing goals", () => {
|
||||
it("Displays a valid goal edit cell", () => {
|
||||
const goal: IGoalEditInput = {
|
||||
value: 1,
|
||||
year: 2021,
|
||||
type: Product2.Vehicle,
|
||||
departmentId: 99,
|
||||
personId: "[email protected]",
|
||||
month: 1
|
||||
};
|
||||
const onAction = () => {};
|
||||
let wrapper = shallow(
|
||||
<GoalEditCell goal={goal} tabIndex={1} requestAction={onAction} />
|
||||
);
|
||||
expect(wrapper.find('[data-test="goal-edit-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,48 @@
|
||||
import React, { useState } from "react";
|
||||
import { Product2 } from "../../shared/enumerations/Product";
|
||||
import { IGoalEditInput } from "../../shared/models/Goal";
|
||||
import GoalService from "../../shared/services/GoalService";
|
||||
|
||||
interface GoalEditorRowProps {
|
||||
goal: IGoalEditInput;
|
||||
tabIndex: number;
|
||||
requestAction: (type: Product2, value: number) => void;
|
||||
}
|
||||
|
||||
const GoalEditCell: React.FC<GoalEditorRowProps> = (
|
||||
props: GoalEditorRowProps
|
||||
) => {
|
||||
const goal = props.goal;
|
||||
const originalValue = goal.value + "";
|
||||
const [newValue, setNewValue] = useState<string>(goal.value + "");
|
||||
|
||||
const pushChange = (value: string) => {
|
||||
const filtered = value.replace(/\D/g, "").trim();
|
||||
props.requestAction(goal.type, parseInt(filtered, 10));
|
||||
setNewValue(filtered);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="input-group input-group-sm" data-test="goal-edit-cell">
|
||||
<input
|
||||
type="text"
|
||||
className={GoalService.getInputCSSClassList(
|
||||
goal.type,
|
||||
originalValue,
|
||||
newValue
|
||||
)}
|
||||
value={newValue}
|
||||
onChange={(event) => pushChange(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">
|
||||
Range: {goal.type.minValue} to {goal.type.maxValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoalEditCell;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IGoalChangeRequest
|
||||
} from "../../../shared/models/Goal";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
|
||||
interface GoalTotalProps {
|
||||
departmentGoals: IDepartmentGoals;
|
||||
pendingChanges: IGoalChangeRequest[];
|
||||
product: Product2;
|
||||
}
|
||||
|
||||
const AnnualProductTotal: React.FC<GoalTotalProps> = (
|
||||
props: GoalTotalProps
|
||||
) => {
|
||||
const total = GoalService.sumDepartmentGoalsForProduct(
|
||||
props.departmentGoals,
|
||||
props.product
|
||||
);
|
||||
const totalPending = GoalService.sumPendingDepartmentGoalsForProduct(
|
||||
props.departmentGoals,
|
||||
props.pendingChanges,
|
||||
props.product
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{GoalService.formatMonthlyGoal(total, props.product)}
|
||||
{totalPending !== total
|
||||
? " -> " +
|
||||
GoalService.formatMonthlyGoal(totalPending, props.product)
|
||||
: ""}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnnualProductTotal;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import DepartmentGoalsControls from "./DepartmentGoalsControls";
|
||||
import { IDepartmentGoalsInput } from "../../../shared/models/Goal";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
|
||||
describe("A component for controlling the department goals view", () => {
|
||||
it("Displays a valid department goal controls", () => {
|
||||
const input: IDepartmentGoalsInput = {
|
||||
year: 2021,
|
||||
departmentId: "99"
|
||||
};
|
||||
const onChange = () => {};
|
||||
const onSave = () => {};
|
||||
const departments: IDepartment[] = [
|
||||
{
|
||||
state: "Active",
|
||||
departmentName: "Test Dept",
|
||||
id: 99,
|
||||
businessLineCode: "TEST",
|
||||
departmentId: 56
|
||||
}
|
||||
];
|
||||
let wrapper = shallow(
|
||||
<DepartmentGoalsControls
|
||||
inputForm={input}
|
||||
onChange={onChange}
|
||||
onSave={onSave}
|
||||
departments={departments}
|
||||
allowSave={false}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find('[data-test="department-goals-controls"]').length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IDepartmentGoalsInput,
|
||||
IGoalChangeRequest
|
||||
} from "../../../shared/models/Goal";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import XLSX from "xlsx";
|
||||
import GoalsDisplay from "../../../components/goal/GoalsDisplay";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import { FormGroup } from "reactstrap";
|
||||
|
||||
export interface IProps {
|
||||
inputForm: IDepartmentGoalsInput;
|
||||
departments: IDepartment[];
|
||||
onChange: (newInput: IDepartmentGoalsInput) => void;
|
||||
onSave: () => void;
|
||||
onExport: () => void;
|
||||
onImport: (file: File) => void;
|
||||
display?: string;
|
||||
allowSave: boolean;
|
||||
}
|
||||
|
||||
const DepartmentGoalsControls: React.FC<IProps> = (input: IProps) => {
|
||||
const [departmentId, setDepartmentId] = useState(
|
||||
input.inputForm.departmentId
|
||||
);
|
||||
const [year, setYear] = useState(input.inputForm.year);
|
||||
|
||||
useEffect(() => {
|
||||
input.onChange({
|
||||
year: year,
|
||||
departmentId: departmentId
|
||||
});
|
||||
}, [departmentId, year]);
|
||||
|
||||
const years = [year - 2, year - 1, year, year + 1, year + 2];
|
||||
|
||||
function fileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const fileDom = e.target;
|
||||
if (fileDom && fileDom.files && fileDom.files.length) {
|
||||
input.onImport(fileDom.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="navbar navbar-expand-lg navbar-light bg-light"
|
||||
data-test="department-goals-controls"
|
||||
>
|
||||
<a className="navbar-brand" href="#">
|
||||
Department Goals for{" "}
|
||||
<select
|
||||
id="deptSelect"
|
||||
value={departmentId}
|
||||
onChange={(event) => {
|
||||
setDepartmentId(event.target.value);
|
||||
}}
|
||||
>
|
||||
{input.departments.map((dept: IDepartment) => (
|
||||
<option
|
||||
key={dept.id}
|
||||
value={dept.id}
|
||||
disabled={dept.state === "Inactive"}
|
||||
>
|
||||
{dept.departmentName}{" "}
|
||||
{dept.state === "Inactive" ? "(Inactive)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>{" "}
|
||||
for{" "}
|
||||
<select
|
||||
id="deptGoalYear"
|
||||
value={year}
|
||||
onChange={(event) => {
|
||||
setYear(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
{years.map((year: number) => (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</a>
|
||||
<div id="navbarSupportedContent">
|
||||
<button
|
||||
className="btn btn-outline-success ml-2"
|
||||
type="button"
|
||||
onClick={input.onSave}
|
||||
disabled={!input.allowSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{/*<button className="btn btn-outline-warning ml-2" type="button">Discard</button>*/}
|
||||
<button
|
||||
className="btn btn-outline-info ml-2"
|
||||
type="button"
|
||||
onClick={input.onExport}
|
||||
>
|
||||
{
|
||||
Translate.Get.containers.goals.department
|
||||
.departmentGoalsControls.exportButton
|
||||
}
|
||||
</button>
|
||||
<span className="ml-2 btn btn-sm btn-outline-info">
|
||||
Import Goals:
|
||||
<input
|
||||
type="file"
|
||||
name="File Upload"
|
||||
id="goalFileUpload"
|
||||
className="ml-1"
|
||||
onChange={(e) => fileChange(e)}
|
||||
accept=".xls,.xlsx"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ margin: "1em" }}>{input.display}</span>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentGoalsControls;
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import * as reactRedux from "react-redux";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import GoalTable from "../../../components/goal/GoalTable";
|
||||
import DepartmentGoalsDetail, {
|
||||
IDepartmentGoalsDetailProps
|
||||
} from "./DepartmentGoalsDetail";
|
||||
|
||||
describe("DepartmentGoalsDetail", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
it("should display waiting while pending", () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(testGoalsDataLoading);
|
||||
|
||||
const detailInput: IDepartmentGoalsDetailProps = {
|
||||
input: {
|
||||
departmentId: "1",
|
||||
year: 2021
|
||||
}
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DepartmentGoalsDetail input={detailInput.input} />
|
||||
);
|
||||
expect(wrapper.find("div").html().toString()).toBe(
|
||||
"<div>Please Wait.... </div>"
|
||||
);
|
||||
});
|
||||
|
||||
it("should display data when successful", () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(testGoalsData);
|
||||
|
||||
const detailInput: IDepartmentGoalsDetailProps = {
|
||||
input: {
|
||||
departmentId: "1",
|
||||
year: 2021
|
||||
}
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<DepartmentGoalsDetail input={detailInput.input} />
|
||||
);
|
||||
expect(wrapper.find("GoalTable").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Simulating IGoalState
|
||||
const testGoalsDataLoading = {
|
||||
userGoals: {
|
||||
state: AsyncState.Loading,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
// Simulating IGoalState
|
||||
const testGoalsData = {
|
||||
userGoals: {
|
||||
state: AsyncState.Success,
|
||||
data: [
|
||||
{
|
||||
id: 1077,
|
||||
departmentId: 103,
|
||||
personId: null,
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:22.5617363",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:22.5616949",
|
||||
createdByType: "Associate"
|
||||
},
|
||||
{
|
||||
id: 1078,
|
||||
departmentId: 103,
|
||||
personId: null,
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:23.5158345",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:23.5157728",
|
||||
createdByType: "Associate"
|
||||
},
|
||||
{
|
||||
id: 1079,
|
||||
departmentId: 103,
|
||||
personId: null,
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:23.5228421",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:23.5226656",
|
||||
createdByType: "Associate"
|
||||
},
|
||||
{
|
||||
id: 1080,
|
||||
departmentId: 103,
|
||||
personId: null,
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:23.5330519",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:23.5329406",
|
||||
createdByType: "Associate"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { queryGoals } from "../../../actions/GoalActions";
|
||||
import { IAppState } from "../../../store/Store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { IGoalState } from "../../../reducers/GoalReducer";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import GoalTable from "../../../components/goal/GoalTable";
|
||||
import { IDepartmentGoalsInput } from "../../../shared/models/Goal";
|
||||
import { IMonthRange } from "../../../shared/types/IMonthRange";
|
||||
|
||||
export interface IDepartmentGoalsDetailProps {
|
||||
input: IDepartmentGoalsInput;
|
||||
}
|
||||
|
||||
const DepartmentGoalsDetail: React.FC<IDepartmentGoalsDetailProps> = (
|
||||
props
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const goalState = useSelector<IAppState, IGoalState>(
|
||||
(state) => state.goalState
|
||||
);
|
||||
const departmentGoals = goalState.userGoals;
|
||||
|
||||
useEffect(() => {
|
||||
const range: IMonthRange = {
|
||||
fromMonth: 1,
|
||||
fromYear: props.input.year,
|
||||
toMonth: 12,
|
||||
toYear: props.input.year
|
||||
};
|
||||
const includePersonnel = false;
|
||||
dispatch(queryGoals(range, includePersonnel, props.input.departmentId));
|
||||
}, []);
|
||||
|
||||
return departmentGoals.state == AsyncState.Success &&
|
||||
departmentGoals.data ? (
|
||||
<GoalTable goals={departmentGoals.data} />
|
||||
) : (
|
||||
<div>Please Wait.... </div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentGoalsDetail;
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import DepartmentGoalsEditor, {
|
||||
IDepartmentGoalsEditorProps
|
||||
} from "./DepartmentGoalsEditor";
|
||||
import * as reactRedux from "react-redux";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
|
||||
describe("DepartmentGoalsEditor", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const exportXlsSpy = jest.spyOn(
|
||||
GoalService,
|
||||
"exportAllGoalsForYearByProduct"
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
it("should call the export service with product goals and a monthly totals", () => {
|
||||
useSelectorMock.mockImplementation(() => ({
|
||||
departmentGoals: {
|
||||
data: {
|
||||
departmentId: 1,
|
||||
year: 2021,
|
||||
monthlyGoals: [
|
||||
{
|
||||
productGoals: {
|
||||
CreditCard: 3,
|
||||
GAPInsurance: 8,
|
||||
LIC: 9,
|
||||
Mortgage: 11,
|
||||
NewChecking: 12,
|
||||
Vehicle: 13
|
||||
}
|
||||
},
|
||||
{
|
||||
productGoals: {
|
||||
CreditCard: 3,
|
||||
GAPInsurance: 8,
|
||||
LIC: 9,
|
||||
Mortgage: 11,
|
||||
NewChecking: 12,
|
||||
Vehicle: 13
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}));
|
||||
const wrapper = renderDepartmentGoalsEditor();
|
||||
wrapper.find("DepartmentGoalsControls").simulate("export");
|
||||
expect(exportXlsSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function renderDepartmentGoalsEditor(args = {}) {
|
||||
const defaultProps: IDepartmentGoalsEditorProps = {
|
||||
departments: [
|
||||
{
|
||||
businessLineCode: "BCHOPS",
|
||||
departmentName: "TestBranch",
|
||||
id: 1,
|
||||
state: "Active"
|
||||
}
|
||||
]
|
||||
} as IDepartmentGoalsEditorProps;
|
||||
|
||||
const props = { ...defaultProps, ...args };
|
||||
return shallow(<DepartmentGoalsEditor {...props} />);
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getDepartmentGoals } from "../../../actions/GoalActions";
|
||||
import { IAppState } from "../../../store/Store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
import { IGoalState } from "../../../reducers/GoalReducer";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import DepartmentGoalsTable from "./DepartmentGoalsTable";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IDepartmentGoalsInput,
|
||||
IGoalChangeRequest
|
||||
} from "../../../shared/models/Goal";
|
||||
import DepartmentGoalsControls from "./DepartmentGoalsControls";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import TabView from "../../../components/tab-view/TabView";
|
||||
import DepartmentGoalsDetail from "./DepartmentGoalsDetail";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import XLSX from "xlsx";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
const DepartmentGoalsTranslate = Translate.Get.containers.goals.department;
|
||||
|
||||
export interface IDepartmentGoalsEditorProps {
|
||||
departments: IDepartment[];
|
||||
}
|
||||
|
||||
enum ImportState {
|
||||
NONE,
|
||||
LOADING,
|
||||
PROCESSING,
|
||||
SUBMITTING
|
||||
}
|
||||
|
||||
const DepartmentGoalsEditor: React.FC<IDepartmentGoalsEditorProps> = (
|
||||
props
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
const [input, setInput] = useState<IDepartmentGoalsInput>({
|
||||
departmentId: props.departments[0].id + "",
|
||||
year: moment().year()
|
||||
});
|
||||
const [importState, setImportState] = useState<ImportState>(
|
||||
ImportState.NONE
|
||||
);
|
||||
const [lastSave, setLastSave] = useState<string>("");
|
||||
|
||||
const [pendingChanges, setPendingChanges] = useState<IGoalChangeRequest[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const goalState = useSelector<IAppState, IGoalState>(
|
||||
(state) => state.goalState
|
||||
);
|
||||
const departmentGoals = goalState.departmentGoals;
|
||||
|
||||
useEffect(() => {
|
||||
setPendingChanges([]);
|
||||
dispatch(getDepartmentGoals(input));
|
||||
}, [input, lastSave]);
|
||||
|
||||
const queueChanges = (label: string, type: Product2, value: number) => {
|
||||
const index = pendingChanges.findIndex(
|
||||
(element: IGoalChangeRequest) =>
|
||||
element.product == type &&
|
||||
element.startDate.month() == GoalService.getMonthIndex(label)
|
||||
);
|
||||
const change = GoalService.convertToGoalChanges(
|
||||
label,
|
||||
type,
|
||||
value,
|
||||
parseInt(input.departmentId, 10),
|
||||
input.year
|
||||
);
|
||||
const changes = pendingChanges.slice();
|
||||
if (index >= 0) {
|
||||
if (isNaN(value)) {
|
||||
changes.splice(index, 1); // remove it.
|
||||
} else {
|
||||
changes.splice(index, 1, change); // replace
|
||||
}
|
||||
} else {
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
changes.push(change); // add
|
||||
}
|
||||
setPendingChanges(changes);
|
||||
};
|
||||
|
||||
const isValidChanges = (): boolean => {
|
||||
if (pendingChanges && pendingChanges.length > 0) {
|
||||
return GoalService.isValidDepartmentChanges(pendingChanges);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
if (isValidChanges()) {
|
||||
GoalService.savePendingChanges(pendingChanges).then(() => {
|
||||
setLastSave("Changes Saved " + moment().format("LTS"));
|
||||
setImportState(ImportState.NONE);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const importCsv = (file: File) => {
|
||||
setImportState(ImportState.LOADING);
|
||||
const reader = new FileReader();
|
||||
let existingData: IDepartmentGoals[] = [];
|
||||
|
||||
reader.onerror = function () {
|
||||
setImportState(ImportState.NONE);
|
||||
};
|
||||
reader.onload = (e) => {
|
||||
setImportState(ImportState.PROCESSING);
|
||||
const wb = XLSX.read(e.target!.result, { type: "array" });
|
||||
/* Get first worksheet */
|
||||
let allChanges: IGoalChangeRequest[] = [];
|
||||
wb.SheetNames.forEach((sheetName, sheetIdx) => {
|
||||
const ws = wb.Sheets[sheetName];
|
||||
const data = XLSX.utils.sheet_to_json(ws, { header: 1 });
|
||||
allChanges.push(
|
||||
...GoalService.ImportGoalSheet(
|
||||
sheetName,
|
||||
input.year,
|
||||
data,
|
||||
existingData
|
||||
)
|
||||
);
|
||||
});
|
||||
if (allChanges.length) {
|
||||
setPendingChanges(allChanges);
|
||||
setImportState(ImportState.SUBMITTING);
|
||||
} else {
|
||||
setImportState(ImportState.NONE);
|
||||
}
|
||||
};
|
||||
GoalService.getAllDepartmentGoals(input.year).then(
|
||||
(allDepartmentsAllGoals) => {
|
||||
if (allDepartmentsAllGoals.data) {
|
||||
existingData = allDepartmentsAllGoals.data;
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const exportCsv = () => {
|
||||
const year = input.year;
|
||||
const departments = props.departments;
|
||||
GoalService.exportAllGoalsForYearByProduct(year, departments);
|
||||
};
|
||||
|
||||
const getControls = () => {
|
||||
switch (importState) {
|
||||
default:
|
||||
case ImportState.NONE:
|
||||
return (
|
||||
<DepartmentGoalsControls
|
||||
inputForm={input}
|
||||
onChange={(change) => setInput(change)}
|
||||
onSave={saveChanges}
|
||||
onExport={exportCsv}
|
||||
display={lastSave}
|
||||
departments={props.departments}
|
||||
allowSave={isValidChanges()}
|
||||
onImport={importCsv}
|
||||
/>
|
||||
);
|
||||
case ImportState.LOADING:
|
||||
return (
|
||||
<Alert color="info">
|
||||
<p className="m-0">
|
||||
{DepartmentGoalsTranslate.states.loading}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
case ImportState.PROCESSING:
|
||||
return (
|
||||
<Alert color="info">
|
||||
<p className="m-0">
|
||||
{DepartmentGoalsTranslate.states.processing}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
case ImportState.SUBMITTING:
|
||||
return (
|
||||
<Alert color="info">
|
||||
<p className="m-0">
|
||||
Processed {pendingChanges.length} changes.{" "}
|
||||
<button
|
||||
className="btn btn-outline-success ml-2"
|
||||
type="button"
|
||||
onClick={saveChanges}
|
||||
>
|
||||
{DepartmentGoalsTranslate.buttons.confirmSave}
|
||||
</button>
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{getControls()}
|
||||
{departmentGoals.state == AsyncState.Success &&
|
||||
departmentGoals.data ? (
|
||||
<TabView
|
||||
data-test="tabbed-commission-view"
|
||||
pages={[
|
||||
{
|
||||
header: DepartmentGoalsTranslate.tabs.editor,
|
||||
element: (
|
||||
<DepartmentGoalsTable
|
||||
departmentGoals={departmentGoals.data}
|
||||
pendingChanges={pendingChanges}
|
||||
onChange={queueChanges}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: DepartmentGoalsTranslate.tabs.detail,
|
||||
element: <DepartmentGoalsDetail input={input} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<div>Please Wait.... </div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentGoalsEditor;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DepartmentGoalsEditor from "./DepartmentGoalsEditor";
|
||||
import DepartmentService from "../../../shared/services/DepartmentService";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
const DepartmentGoalsPage: React.FC = () => {
|
||||
const [departments, setDepartments] = useState<IDepartment[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
DepartmentService.getAllDepartments(true)
|
||||
.then((response) => {
|
||||
setDepartments(response.data || []);
|
||||
})
|
||||
.catch(() => {
|
||||
setDepartments([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (departments && departments.length === 0) {
|
||||
return (
|
||||
<Alert color="warning" data-test="not-found">
|
||||
No departments found for your role
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return departments && departments.length > 0 ? (
|
||||
<DepartmentGoalsEditor departments={departments} />
|
||||
) : (
|
||||
<div>Please Wait.... </div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentGoalsPage;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IGoalChangeRequest,
|
||||
IPersonnelGoalsInput
|
||||
} from "../../../shared/models/Goal";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import DepartmentGoalsTable from "./DepartmentGoalsTable";
|
||||
|
||||
describe("A Table for editing department goals", () => {
|
||||
it("Displays a valid department goal edit table", () => {
|
||||
const departmentGoals: IDepartmentGoals = {
|
||||
departmentId: 99,
|
||||
year: 2021,
|
||||
monthlyGoals: [
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} }
|
||||
]
|
||||
};
|
||||
const pendingChanges: IGoalChangeRequest[] = [];
|
||||
const input: IPersonnelGoalsInput = {
|
||||
year: 2021,
|
||||
departmentId: "99",
|
||||
month: 2
|
||||
};
|
||||
const onChange = () => {};
|
||||
let wrapper = shallow(
|
||||
<DepartmentGoalsTable
|
||||
departmentGoals={departmentGoals}
|
||||
onChange={onChange}
|
||||
pendingChanges={pendingChanges}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find("[id='department-goals-table']").length).toBe(1);
|
||||
});
|
||||
|
||||
describe("Products with department goals", () => {
|
||||
let departmentGoals: IDepartmentGoals;
|
||||
let productWithDepartmentGoals: Product2[];
|
||||
|
||||
beforeAll(() => {
|
||||
departmentGoals = {
|
||||
departmentId: 99,
|
||||
year: 2021,
|
||||
monthlyGoals: [
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} }
|
||||
]
|
||||
};
|
||||
|
||||
productWithDepartmentGoals = Product2.Products.filter(
|
||||
(product) => product.hasDepartmentGoals
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays titles in the table header for products that have department goals", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentGoalsTable
|
||||
departmentGoals={departmentGoals}
|
||||
onChange={() => {}}
|
||||
pendingChanges={[]}
|
||||
/>
|
||||
);
|
||||
const productTitles = productWithDepartmentGoals.map(
|
||||
(product) => product.title
|
||||
);
|
||||
const tableHeaders = wrapper.find("thead > tr > th").slice(1);
|
||||
|
||||
expect(tableHeaders.length).toEqual(
|
||||
productWithDepartmentGoals.length
|
||||
);
|
||||
tableHeaders.forEach((th) =>
|
||||
expect(productTitles).toContain(th.text())
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays only products with department goals in each GoalEditCell", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentGoalsTable
|
||||
departmentGoals={departmentGoals}
|
||||
onChange={() => {}}
|
||||
pendingChanges={[]}
|
||||
/>
|
||||
);
|
||||
const tableRows = wrapper.find("tbody > tr");
|
||||
|
||||
tableRows.forEach((tr) => {
|
||||
const goalEditCells = tr.find("GoalEditCell");
|
||||
expect(goalEditCells.length).toBe(
|
||||
productWithDepartmentGoals.length
|
||||
);
|
||||
goalEditCells.forEach((cell) => {
|
||||
const goal: { type: Product2 } = cell.prop("goal");
|
||||
expect(goal.type.hasDepartmentGoals).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("Displays only products with department goals in each AnnualProductTotal cell ", () => {
|
||||
const wrapper = shallow(
|
||||
<DepartmentGoalsTable
|
||||
departmentGoals={departmentGoals}
|
||||
onChange={() => {}}
|
||||
pendingChanges={[]}
|
||||
/>
|
||||
);
|
||||
const productsInFooter: Product2[] = wrapper
|
||||
.find("tfoot AnnualProductTotal")
|
||||
.map((cell) => cell.prop("product"));
|
||||
|
||||
expect(productsInFooter.length).toEqual(
|
||||
productWithDepartmentGoals.length
|
||||
);
|
||||
productsInFooter.forEach((product) =>
|
||||
expect(product.hasDepartmentGoals).toBe(true)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IGoalChangeRequest
|
||||
} from "../../../shared/models/Goal";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import GoalEditCell from "../GoalEditCell";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
import AnnualProductTotal from "./AnnualProductTotal";
|
||||
|
||||
export interface IProps {
|
||||
departmentGoals: IDepartmentGoals;
|
||||
pendingChanges: IGoalChangeRequest[];
|
||||
onChange: (label: string, type: Product2, value: number) => void;
|
||||
}
|
||||
|
||||
const DepartmentGoalsTable: React.FC<IProps> = (props: IProps) => {
|
||||
const onAction = (label: string, type: Product2, value: number) => {
|
||||
props.onChange(label, type, value);
|
||||
};
|
||||
|
||||
const productsWithDepartmentGoals = Product2.Products.filter(
|
||||
(product) => product.hasDepartmentGoals
|
||||
);
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped tablecenterCSS tablecenterheadCSS"
|
||||
id="department-goals-table"
|
||||
>
|
||||
<thead className="thead-light">
|
||||
<tr>
|
||||
<th scope="col">Month</th>
|
||||
{productsWithDepartmentGoals.map((product) => (
|
||||
<th key={product.code} scope="col" title={product.code}>
|
||||
{product.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="form-group">
|
||||
{GoalService.Months.map((month, index: number) => (
|
||||
<tr key={month}>
|
||||
<td>{month}</td>
|
||||
{productsWithDepartmentGoals.map((product, index2) => (
|
||||
<td
|
||||
key={month + "_" + product.code}
|
||||
scope="col"
|
||||
title="figure me out"
|
||||
>
|
||||
<GoalEditCell
|
||||
goal={{
|
||||
year: props.departmentGoals.year,
|
||||
type: product,
|
||||
value:
|
||||
props.departmentGoals.monthlyGoals[
|
||||
index
|
||||
].productGoals[product.code],
|
||||
departmentId:
|
||||
props.departmentGoals.departmentId
|
||||
}}
|
||||
tabIndex={index + (index2 + 1) * 100}
|
||||
requestAction={(
|
||||
type: Product2,
|
||||
value: number
|
||||
) => onAction(month, type, value)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="alert alert-secondary">
|
||||
<td>Totals</td>
|
||||
{productsWithDepartmentGoals.map((product) => (
|
||||
<td key={"total_" + product.code} scope="col">
|
||||
<AnnualProductTotal
|
||||
departmentGoals={props.departmentGoals}
|
||||
pendingChanges={props.pendingChanges}
|
||||
product={product}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentGoalsTable;
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import MonthlyProductTotal from "./MonthlyProductTotal";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import { IEmployee } from "../../../shared/models/Employee";
|
||||
import moment from "moment";
|
||||
import { UserType } from "../../../shared/models/Auditable";
|
||||
|
||||
describe("A component for monthly product totals", () => {
|
||||
const mockUser: IEmployee = {
|
||||
role: {
|
||||
name: "baseRole",
|
||||
rank: 1,
|
||||
permissions: [],
|
||||
description: "fake role",
|
||||
hasCommissions: true
|
||||
},
|
||||
departments: [
|
||||
{
|
||||
departmentId: 55,
|
||||
employeeId: "11771",
|
||||
isHome: true,
|
||||
note: "fake home assignment",
|
||||
startDate: moment(),
|
||||
endDate: moment(),
|
||||
created: moment(),
|
||||
createdBy: "nobody",
|
||||
createdByType: UserType.System,
|
||||
lastUpdated: moment(),
|
||||
id: 5,
|
||||
lastUpdatedBy: "nobody",
|
||||
lastUpdatedByType: UserType.System
|
||||
}
|
||||
],
|
||||
baseRole: "none",
|
||||
homeDepartment: "none",
|
||||
personId: 555,
|
||||
adEmail: "[email protected]",
|
||||
employeeId: "11771",
|
||||
fullName: "Test McTester",
|
||||
additionalPermissions: []
|
||||
};
|
||||
|
||||
it("Displays a valid total when everything is -zero-", () => {
|
||||
const data = {
|
||||
personnelGoals: {
|
||||
departmentId: 5,
|
||||
year: 2020,
|
||||
month: 1,
|
||||
personnel: [mockUser], // IEmployee[];
|
||||
goals: {
|
||||
"11771": {
|
||||
productGoals: {
|
||||
NewChecking: 0,
|
||||
CreditCard: 0,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
Vehicle: 0
|
||||
}
|
||||
}
|
||||
} // PerPersonGoalType;
|
||||
}, // IPersonnelGoals;
|
||||
pendingChanges: [], // IGoalChangeRequest[];
|
||||
product: Product2.Vehicle,
|
||||
departmentTotal: 0
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MonthlyProductTotal
|
||||
product={data.product}
|
||||
departmentTotal={data.departmentTotal}
|
||||
personnelGoals={data.personnelGoals}
|
||||
pendingChanges={data.pendingChanges}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.html()).toBe('<span class="text-normal">0 (0%)</span>');
|
||||
});
|
||||
|
||||
it("Displays a valid total when everything is 100%", () => {
|
||||
const data = {
|
||||
personnelGoals: {
|
||||
departmentId: 5,
|
||||
year: 2020,
|
||||
month: 1,
|
||||
personnel: [mockUser], // IEmployee[];
|
||||
goals: {
|
||||
"11771": {
|
||||
productGoals: {
|
||||
NewChecking: 5,
|
||||
CreditCard: 5,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
Vehicle: 25
|
||||
}
|
||||
}
|
||||
} // PerPersonGoalType;
|
||||
}, // IPersonnelGoals;
|
||||
pendingChanges: [], // IGoalChangeRequest[];
|
||||
product: Product2.Vehicle,
|
||||
departmentTotal: 25
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MonthlyProductTotal
|
||||
product={data.product}
|
||||
departmentTotal={data.departmentTotal}
|
||||
personnelGoals={data.personnelGoals}
|
||||
pendingChanges={data.pendingChanges}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.html()).toBe(
|
||||
'<span class="text-normal">25 (100%)</span>'
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays a valid total when everything is 50%", () => {
|
||||
const data = {
|
||||
personnelGoals: {
|
||||
departmentId: 5,
|
||||
year: 2020,
|
||||
month: 1,
|
||||
personnel: [mockUser], // IEmployee[];
|
||||
goals: {
|
||||
"11771": {
|
||||
productGoals: {
|
||||
NewChecking: 3,
|
||||
CreditCard: 510,
|
||||
GAPInsurance: 610,
|
||||
LIC: 9,
|
||||
Mortgage: 4,
|
||||
Vehicle: 10
|
||||
}
|
||||
}
|
||||
} // PerPersonGoalType;
|
||||
}, // IPersonnelGoals;
|
||||
pendingChanges: [], // IGoalChangeRequest[];
|
||||
product: Product2.Vehicle,
|
||||
departmentTotal: 20
|
||||
};
|
||||
const wrapper = mount(
|
||||
<MonthlyProductTotal
|
||||
product={data.product}
|
||||
departmentTotal={data.departmentTotal}
|
||||
personnelGoals={data.personnelGoals}
|
||||
pendingChanges={data.pendingChanges}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.html()).toBe(
|
||||
'<span class="text-normal">10 (50%)</span>'
|
||||
);
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IGoalChangeRequest,
|
||||
IPersonnelGoals
|
||||
} from "../../../shared/models/Goal";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
|
||||
interface GoalTotalProps {
|
||||
personnelGoals: IPersonnelGoals;
|
||||
pendingChanges: IGoalChangeRequest[];
|
||||
product: Product2;
|
||||
departmentTotal: number;
|
||||
}
|
||||
|
||||
const MonthlyProductTotal: React.FC<GoalTotalProps> = (
|
||||
props: GoalTotalProps
|
||||
) => {
|
||||
const total = GoalService.sumPersonnelGoalsForProduct(
|
||||
props.personnelGoals,
|
||||
props.product
|
||||
);
|
||||
const totalPending = GoalService.sumPendingPersonnelGoalsForProduct(
|
||||
props.personnelGoals,
|
||||
props.pendingChanges,
|
||||
props.product
|
||||
);
|
||||
|
||||
let percentage = Math.floor(
|
||||
((totalPending !== total ? totalPending : total) /
|
||||
props.departmentTotal) *
|
||||
100
|
||||
);
|
||||
if (isNaN(percentage)) {
|
||||
percentage = 0;
|
||||
}
|
||||
const percentStr = " (" + percentage + "%)";
|
||||
let outputStr =
|
||||
GoalService.formatMonthlyGoal(total, props.product) + percentStr;
|
||||
let className = "text-normal";
|
||||
|
||||
if (totalPending !== total) {
|
||||
outputStr =
|
||||
GoalService.formatMonthlyGoal(total, props.product) +
|
||||
" -> " +
|
||||
GoalService.formatMonthlyGoal(totalPending, props.product) +
|
||||
percentStr;
|
||||
className = percentage < 100 ? "text-danger" : "text-success";
|
||||
}
|
||||
|
||||
return <span className={className}>{outputStr}</span>;
|
||||
};
|
||||
|
||||
export default MonthlyProductTotal;
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { mount, shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IPersonnelGoalsInput } from "../../../shared/models/Goal";
|
||||
import PersonnelGoalsControls from "./PersonnelGoalsControls";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import { IGoalDispersalReceiptItem } from "../../../shared/models/GoalDispersal";
|
||||
|
||||
describe("A component for controlling personnel goals view", () => {
|
||||
it("Displays a valid personnel goals controls", () => {
|
||||
const input: IPersonnelGoalsInput = {
|
||||
year: 2021,
|
||||
departmentId: "99",
|
||||
month: 2,
|
||||
isShowAllPersonnelChecked: false
|
||||
};
|
||||
const onChange = () => {};
|
||||
const onSave = () => {};
|
||||
const onExport = () => {};
|
||||
const departments: IDepartment[] = [
|
||||
{
|
||||
state: "Active",
|
||||
departmentName: "Test Dept",
|
||||
id: 99,
|
||||
businessLineCode: "TEST",
|
||||
departmentId: 99
|
||||
}
|
||||
];
|
||||
const receiptContainer = {
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
departmentId: 99,
|
||||
goalDispersalErrors: []
|
||||
}
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<PersonnelGoalsControls
|
||||
inputForm={input}
|
||||
onChange={onChange}
|
||||
onSave={onSave}
|
||||
onExport={onExport}
|
||||
departments={departments}
|
||||
allowSave={false}
|
||||
dispersalReceiptContainer={receiptContainer}
|
||||
submitDispersalViaExtrapolation={(request) => {}}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
wrapper.find("[data-test='personnel-goals-controls']").length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("Calls onChange show all personnel is changed", async () => {
|
||||
const input: IPersonnelGoalsInput = {
|
||||
year: 2021,
|
||||
departmentId: "99",
|
||||
month: 2,
|
||||
isShowAllPersonnelChecked: false
|
||||
};
|
||||
const stubOnChange = jest.fn();
|
||||
const onSave = () => {};
|
||||
const onExport = () => {};
|
||||
const departments: IDepartment[] = [
|
||||
{
|
||||
state: "Active",
|
||||
departmentName: "Test Dept",
|
||||
id: 99,
|
||||
businessLineCode: "TEST",
|
||||
departmentId: 99
|
||||
}
|
||||
];
|
||||
const receiptContainer = {
|
||||
state: AsyncState.Success,
|
||||
data: {
|
||||
departmentId: 99,
|
||||
goalDispersalErrors: []
|
||||
}
|
||||
};
|
||||
const wrapper = mount(
|
||||
<PersonnelGoalsControls
|
||||
inputForm={input}
|
||||
onChange={stubOnChange}
|
||||
onSave={onSave}
|
||||
onExport={onExport}
|
||||
departments={departments}
|
||||
allowSave={false}
|
||||
dispersalReceiptContainer={receiptContainer}
|
||||
submitDispersalViaExtrapolation={(request) => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
stubOnChange.mockClear();
|
||||
expect(stubOnChange).not.toHaveBeenCalled();
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("#toggle-all-personnel input")
|
||||
.simulate("change", { target: { checked: true } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(stubOnChange).toHaveBeenCalledTimes(1);
|
||||
expect(stubOnChange).toHaveBeenLastCalledWith({
|
||||
year: 2021,
|
||||
departmentId: "99",
|
||||
month: 2,
|
||||
isShowAllPersonnelChecked: true
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find("#toggle-all-personnel input")
|
||||
.simulate("change", { target: { checked: false } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
expect(stubOnChange).toHaveBeenCalledTimes(2);
|
||||
expect(stubOnChange).toHaveBeenLastCalledWith({
|
||||
year: 2021,
|
||||
departmentId: "99",
|
||||
month: 2,
|
||||
isShowAllPersonnelChecked: false
|
||||
});
|
||||
});
|
||||
});
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IPersonnelGoalsInput } from "../../../shared/models/Goal";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import { CustomInput } from "reactstrap";
|
||||
import { Button } from "reactstrap";
|
||||
import DispersalForm from "../../../components/goal/dispersal/DispersalForm";
|
||||
import {
|
||||
AllowedGoalDispersalStrategy,
|
||||
ICustomWeightedDispersalRequest,
|
||||
IGoalDispersalReceipt,
|
||||
IGoalDispersalRequest
|
||||
} from "../../../shared/models/GoalDispersal";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import moment from "moment";
|
||||
import { ICustomWeightedDispersalFormData } from "./../../../shared/models/GoalDispersal";
|
||||
|
||||
export interface IProps {
|
||||
inputForm: IPersonnelGoalsInput;
|
||||
departments: IDepartment[];
|
||||
onChange: (newInput: IPersonnelGoalsInput) => void;
|
||||
onExport: () => void;
|
||||
onSave: () => void;
|
||||
submitDispersalViaExtrapolation: (request: IGoalDispersalRequest) => void;
|
||||
submitCustomWeightedDispersal: (
|
||||
request: ICustomWeightedDispersalRequest
|
||||
) => void;
|
||||
dispersalReceiptContainer: IAsyncStateContainer<IGoalDispersalReceipt>;
|
||||
display?: string;
|
||||
allowSave: boolean;
|
||||
}
|
||||
|
||||
const PersonnelGoalsControls: React.FC<IProps> = (input: IProps) => {
|
||||
const [departmentId, setDepartmentId] = useState(
|
||||
input.inputForm.departmentId
|
||||
);
|
||||
const [year, setYear] = useState(input.inputForm.year);
|
||||
const [month, setMonth] = useState(input.inputForm.month);
|
||||
const [isShowAllPersonnelChecked, setIsShowAllPersonnelChecked] = useState(
|
||||
input.inputForm.isShowAllPersonnelChecked
|
||||
);
|
||||
const [isDispersalFormOpen, toggleDispersalForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
input.onChange({
|
||||
year: year,
|
||||
departmentId: departmentId,
|
||||
month: month,
|
||||
isShowAllPersonnelChecked: isShowAllPersonnelChecked
|
||||
});
|
||||
toggleDispersalForm(false);
|
||||
}, [departmentId, year, month, isShowAllPersonnelChecked]);
|
||||
|
||||
const years = [year - 2, year - 1, year, year + 1, year + 2];
|
||||
|
||||
//Expand if add more strategies, expand if requires additional arguments
|
||||
const handleStatelessDispersal = (
|
||||
strategy: AllowedGoalDispersalStrategy
|
||||
) => {
|
||||
switch (strategy) {
|
||||
default:
|
||||
input.submitDispersalViaExtrapolation({
|
||||
departmentId: departmentId,
|
||||
month: month,
|
||||
year: year
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomWeightedDispersal = (
|
||||
formData: ICustomWeightedDispersalFormData
|
||||
) => {
|
||||
input.submitCustomWeightedDispersal({
|
||||
departmentId: departmentId,
|
||||
month: month,
|
||||
year: year,
|
||||
percentageByRole: formData.weightsByRole,
|
||||
dispersalProduct: formData.dispersalProduct
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column bg-light">
|
||||
<nav
|
||||
className="navbar navbar-expand-lg navbar-light "
|
||||
data-test="personnel-goals-controls"
|
||||
>
|
||||
<a className="navbar-brand" href="#">
|
||||
Personnel Goals for{" "}
|
||||
<select
|
||||
id="deptSelect"
|
||||
value={departmentId}
|
||||
onChange={(event) => {
|
||||
setDepartmentId(event.target.value);
|
||||
}}
|
||||
>
|
||||
{input.departments.map((dept: IDepartment) => (
|
||||
<option
|
||||
key={dept.id}
|
||||
value={dept.id}
|
||||
disabled={dept.state === "Inactive"}
|
||||
>
|
||||
{dept.departmentName}{" "}
|
||||
{dept.state === "Inactive" ? "(Inactive)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>{" "}
|
||||
for month{" "}
|
||||
<select
|
||||
id="deptGoalMonth"
|
||||
value={month}
|
||||
onChange={(event) => {
|
||||
setMonth(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
{GoalService.Months.map(
|
||||
(monthStr: string, monthNum: number) => (
|
||||
<option key={monthNum} value={monthNum + 1}>
|
||||
{monthStr}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
</select>{" "}
|
||||
<select
|
||||
id="deptGoalYear"
|
||||
value={year}
|
||||
onChange={(event) => {
|
||||
setYear(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
{years.map((year: number) => (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</a>
|
||||
<div>
|
||||
<CustomInput
|
||||
className="ml-2"
|
||||
type="checkbox"
|
||||
id="toggle-all-personnel"
|
||||
checked={isShowAllPersonnelChecked}
|
||||
onChange={(event) => {
|
||||
setIsShowAllPersonnelChecked(event.target.checked);
|
||||
}}
|
||||
label="Show all personnel"
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
<div id="navbarSupportedContent">
|
||||
<button
|
||||
className="btn btn-outline-success ml-2"
|
||||
type="button"
|
||||
onClick={input.onSave}
|
||||
disabled={!input.allowSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{/*<button className="btn btn-outline-warning ml-2" type="button">Discard</button>*/}
|
||||
<button
|
||||
className="btn btn-outline-info ml-2"
|
||||
type="button"
|
||||
onClick={input.onExport}
|
||||
>
|
||||
{
|
||||
Translate.Get.containers.goals.personnel
|
||||
.personnelGoalsControls.exportButton
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => toggleDispersalForm(!isDispersalFormOpen)}
|
||||
className="ml-auto"
|
||||
color={isDispersalFormOpen ? "danger" : "primary"}
|
||||
outline={!isDispersalFormOpen}
|
||||
>
|
||||
{isDispersalFormOpen
|
||||
? Translate.Get.containers.goals.personnel
|
||||
.personnelGoalsControls.closeDispersal
|
||||
: Translate.Get.containers.goals.personnel
|
||||
.personnelGoalsControls.openDispersal}
|
||||
</Button>
|
||||
<span style={{ margin: "1em" }}>{input.display}</span>
|
||||
</nav>
|
||||
<div>
|
||||
<DispersalForm
|
||||
isOpen={isDispersalFormOpen}
|
||||
confirmStatelessDispersal={(strategy) => {
|
||||
handleStatelessDispersal(strategy);
|
||||
}}
|
||||
submitState={input.dispersalReceiptContainer.state}
|
||||
confirmWeightedDispersal={(formData) => {
|
||||
handleCustomWeightedDispersal(formData);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonnelGoalsControls;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import * as reactRedux from "react-redux";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import PersonnelGoalsDetail, {
|
||||
IPersonnelGoalsDetailProps
|
||||
} from "./PersonnelGoalsDetail";
|
||||
import GoalTable from "../../../components/goal/GoalTable";
|
||||
|
||||
describe("PersonnelGoalsDetail", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
it("should display waiting while pending", () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(testGoalsDataLoading);
|
||||
|
||||
const detailInput: IPersonnelGoalsDetailProps = {
|
||||
input: {
|
||||
departmentId: "1",
|
||||
year: 2021,
|
||||
month: 6,
|
||||
isShowAllPersonnelChecked: true
|
||||
}
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<PersonnelGoalsDetail input={detailInput.input} />
|
||||
);
|
||||
expect(wrapper.find("div").html().toString()).toBe(
|
||||
"<div>Please Wait.... </div>"
|
||||
);
|
||||
});
|
||||
|
||||
it("should display data when successful", () => {
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(testGoalsData);
|
||||
|
||||
const detailInput: IPersonnelGoalsDetailProps = {
|
||||
input: {
|
||||
departmentId: "1",
|
||||
year: 2021,
|
||||
month: 6,
|
||||
isShowAllPersonnelChecked: true
|
||||
}
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<PersonnelGoalsDetail input={detailInput.input} />
|
||||
);
|
||||
expect(wrapper.find("GoalTable").length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Simulating IGoalState
|
||||
const testGoalsDataLoading = {
|
||||
userGoals: {
|
||||
state: AsyncState.Loading,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
// Simulating IGoalState
|
||||
const testGoalsData = {
|
||||
userGoals: {
|
||||
state: AsyncState.Success,
|
||||
data: [
|
||||
{
|
||||
id: 1077,
|
||||
departmentId: 103,
|
||||
personId: "11412",
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:22.5617363",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:22.5616949",
|
||||
createdByType: "Associate"
|
||||
},
|
||||
{
|
||||
id: 1078,
|
||||
departmentId: 103,
|
||||
personId: "C00001",
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:23.5158345",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:23.5157728",
|
||||
createdByType: "Associate"
|
||||
},
|
||||
{
|
||||
id: 1079,
|
||||
departmentId: 103,
|
||||
personId: "11928",
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:23.5228421",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:23.5226656",
|
||||
createdByType: "Associate"
|
||||
},
|
||||
{
|
||||
id: 1080,
|
||||
departmentId: 103,
|
||||
personId: "11685",
|
||||
startDate: "2021-08-01T00:00:00",
|
||||
endDate: "2021-08-31T23:59:59",
|
||||
product: "NewChecking",
|
||||
value: 1,
|
||||
lastUpdatedBy: "George Frick",
|
||||
lastUpdated: "2021-08-02T13:29:23.5330519",
|
||||
lastUpdatedByType: "Associate",
|
||||
createdBy: "George Frick",
|
||||
created: "2021-08-02T13:29:23.5329406",
|
||||
createdByType: "Associate"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { queryGoals } from "../../../actions/GoalActions";
|
||||
import { IAppState } from "../../../store/Store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { IGoalState } from "../../../reducers/GoalReducer";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import GoalTable from "../../../components/goal/GoalTable";
|
||||
import { IPersonnelGoalsInput } from "../../../shared/models/Goal";
|
||||
import { IMonthRange } from "../../../shared/types/IMonthRange";
|
||||
|
||||
export interface IPersonnelGoalsDetailProps {
|
||||
input: IPersonnelGoalsInput;
|
||||
}
|
||||
|
||||
const PersonnelGoalsDetail: React.FC<IPersonnelGoalsDetailProps> = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const goalState = useSelector<IAppState, IGoalState>(
|
||||
(state) => state.goalState
|
||||
);
|
||||
const departmentGoals = goalState.userGoals;
|
||||
|
||||
useEffect(() => {
|
||||
const range: IMonthRange = {
|
||||
fromMonth: props.input.month,
|
||||
fromYear: props.input.year,
|
||||
toMonth: props.input.month,
|
||||
toYear: props.input.year
|
||||
};
|
||||
const includePersonnel = true;
|
||||
dispatch(queryGoals(range, includePersonnel, props.input.departmentId));
|
||||
}, []);
|
||||
|
||||
return departmentGoals.state == AsyncState.Success &&
|
||||
departmentGoals.data ? (
|
||||
<GoalTable goals={departmentGoals.data} />
|
||||
) : (
|
||||
<div>Please Wait.... </div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonnelGoalsDetail;
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import PersonnelGoalsEditor, {
|
||||
IGoalsEditorProps
|
||||
} from "./PersonnelGoalsEditor";
|
||||
import ExportCsvService from "../../../shared/services/ExportCsvService";
|
||||
import * as reactRedux from "react-redux";
|
||||
import PermissionCode from "../../../shared/enumerations/PermissionCode";
|
||||
|
||||
describe("PersonnelGoalsEditor", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
const exportCsvSpy = jest.spyOn(
|
||||
ExportCsvService,
|
||||
"exportMonthlyPersonnelGoalsAsCSV"
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
it("should call the export service with personal goals and totals", () => {
|
||||
useSelectorMock.mockImplementation(() => ({
|
||||
departmentGoals: testdepartmentGoalsData(),
|
||||
personnelGoals: testPersonnelGoalsData()
|
||||
}));
|
||||
|
||||
const wrapper = renderPersonnelGoalsEditor();
|
||||
wrapper.find("PersonnelGoalsControls").simulate("export");
|
||||
|
||||
expect(exportCsvSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
goals: {
|
||||
CreditCard: 3,
|
||||
GAPInsurance: 8,
|
||||
LIC: 9,
|
||||
Mortgage: 11,
|
||||
NewChecking: 12,
|
||||
Vehicle: 13
|
||||
},
|
||||
rowHeader: "Department Goal"
|
||||
},
|
||||
{
|
||||
goals: {
|
||||
NewChecking: 0,
|
||||
CreditCard: 0,
|
||||
GAPInsurance: 0,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
Vehicle: 0
|
||||
},
|
||||
rowHeader: "One, Test E"
|
||||
},
|
||||
{
|
||||
goals: {
|
||||
CreditCard: 0,
|
||||
GAPInsurance: 0,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
NewChecking: 0,
|
||||
Vehicle: 0
|
||||
},
|
||||
rowHeader: "Three, Test"
|
||||
},
|
||||
{
|
||||
goals: {
|
||||
CreditCard: 0,
|
||||
GAPInsurance: 0,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
NewChecking: 0,
|
||||
Vehicle: 0
|
||||
},
|
||||
rowHeader: "Totals"
|
||||
}
|
||||
]);
|
||||
expect(exportCsvSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
function renderPersonnelGoalsEditor(args = {}) {
|
||||
const defaultProps: IGoalsEditorProps = {
|
||||
departments: [
|
||||
{
|
||||
businessLineCode: "BCHOPS",
|
||||
departmentName: "TestBranch",
|
||||
id: 1,
|
||||
state: "Active"
|
||||
}
|
||||
]
|
||||
} as IGoalsEditorProps;
|
||||
|
||||
const props = { ...defaultProps, ...args };
|
||||
return shallow(<PersonnelGoalsEditor {...props} />);
|
||||
}
|
||||
|
||||
const testdepartmentGoalsData = () => ({
|
||||
data: {
|
||||
departmentId: 1,
|
||||
year: 2021,
|
||||
monthlyGoals: [
|
||||
{
|
||||
productGoals: {
|
||||
ATMCard: 1,
|
||||
CheckCard: 2,
|
||||
CreditCard: 3,
|
||||
CreditCardBalanceTransfer: 4,
|
||||
DirectDeposit: 7,
|
||||
GAPInsurance: 8,
|
||||
LIC: 9,
|
||||
MechanicalBreakdownProtection: 10,
|
||||
Mortgage: 11,
|
||||
NewChecking: 12,
|
||||
Vehicle: 13
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const testPersonnelGoalsData = () => ({
|
||||
data: {
|
||||
departmentId: 1,
|
||||
year: 2020,
|
||||
month: 1,
|
||||
personnel: [
|
||||
{
|
||||
employeeId: "1",
|
||||
fullName: "One, Test E",
|
||||
role: {
|
||||
permissions: [{ code: PermissionCode.HAVE_SALES_GOALS }]
|
||||
}
|
||||
},
|
||||
{
|
||||
employeeId: "3",
|
||||
fullName: "Three, Test",
|
||||
role: {
|
||||
permissions: [{ code: PermissionCode.HAVE_SALES_GOALS }]
|
||||
}
|
||||
}
|
||||
],
|
||||
goals: {
|
||||
"1": {
|
||||
productGoals: {
|
||||
ATMCard: 0,
|
||||
CreditCardBalanceTransfer: 0,
|
||||
CheckCard: 0,
|
||||
NewChecking: 0,
|
||||
CreditCard: 0,
|
||||
DirectDeposit: 0,
|
||||
GAPInsurance: 0,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
MechanicalBreakdownProtection: 0,
|
||||
Vehicle: 0
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
productGoals: {
|
||||
ATMCard: 0,
|
||||
CreditCardBalanceTransfer: 0,
|
||||
CheckCard: 0,
|
||||
NewChecking: 0,
|
||||
CreditCard: 0,
|
||||
DirectDeposit: 0,
|
||||
GAPInsurance: 0,
|
||||
LIC: 0,
|
||||
Mortgage: 0,
|
||||
MechanicalBreakdownProtection: 0,
|
||||
Vehicle: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
getDepartmentGoals,
|
||||
getPersonnelGoals
|
||||
} from "../../../actions/GoalActions";
|
||||
import { IAppState } from "../../../store/Store";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import moment from "moment";
|
||||
import { IGoalState } from "../../../reducers/GoalReducer";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import PersonnelGoalsTable from "./PersonnelGoalsTable";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import {
|
||||
IGoalChangeRequest,
|
||||
IPersonnelGoalsInput
|
||||
} from "../../../shared/models/Goal";
|
||||
import PersonnelGoalsControls from "./PersonnelGoalsControls";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import ExportCsvService, {
|
||||
MonthlyPersonnelGoalsCsvData
|
||||
} from "../../../shared/services/ExportCsvService";
|
||||
import TabView from "../../../components/tab-view/TabView";
|
||||
import PersonnelGoalsDetail from "./PersonnelGoalsDetail";
|
||||
import Translate from "../../../translate/Translate";
|
||||
import {
|
||||
ICustomWeightedDispersalRequest,
|
||||
IGoalDispersalReceipt,
|
||||
IGoalDispersalRequest
|
||||
} from "../../../shared/models/GoalDispersal";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import GoalDispersalService from "./../../../shared/services/GoalDispersalService";
|
||||
import DispersalFormAlertBar from "../../../components/goal/dispersal/DispersalFormAlertBar";
|
||||
|
||||
export interface IGoalsEditorProps {
|
||||
departments: IDepartment[];
|
||||
}
|
||||
|
||||
const PersonalGoalsTranslate = Translate.Get.containers.goals.personnel;
|
||||
|
||||
const PersonnelGoalsEditor: React.FC<IGoalsEditorProps> = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [input, setInput] = useState<IPersonnelGoalsInput>({
|
||||
departmentId: props.departments[0].id + "",
|
||||
year: moment().year(),
|
||||
month: moment().month() + 1,
|
||||
isShowAllPersonnelChecked: false
|
||||
});
|
||||
const [lastSave, setLastSave] = useState<string>("");
|
||||
const [pendingChanges, setPendingChanges] = useState<IGoalChangeRequest[]>(
|
||||
[]
|
||||
);
|
||||
const [dispersalReceipt, setDispersalReceipt] = useState<
|
||||
IAsyncStateContainer<IGoalDispersalReceipt>
|
||||
>({ state: AsyncState.Inactive });
|
||||
|
||||
useEffect(() => {
|
||||
setPendingChanges([]);
|
||||
setDispersalReceipt({ state: AsyncState.Inactive });
|
||||
dispatch(getPersonnelGoals(input));
|
||||
dispatch(getDepartmentGoals(input));
|
||||
}, [input, lastSave]);
|
||||
|
||||
const goalState = useSelector<IAppState, IGoalState>(
|
||||
(state) => state.goalState
|
||||
);
|
||||
const personnelGoals = goalState.personnelGoals;
|
||||
const filteredPersonnelGoals = personnelGoals.data
|
||||
? GoalService.filterPersonnelGoalsByShowAll(
|
||||
input.isShowAllPersonnelChecked,
|
||||
personnelGoals.data
|
||||
)
|
||||
: personnelGoals.data;
|
||||
const departmentGoals = goalState.departmentGoals;
|
||||
|
||||
const queueChanges = (person: string, type: Product2, value: number) => {
|
||||
const index = pendingChanges.findIndex(
|
||||
(element: IGoalChangeRequest) =>
|
||||
element.product === type &&
|
||||
element.startDate.year() === input.year &&
|
||||
element.startDate.month() === input.month - 1 &&
|
||||
element.personId === person
|
||||
);
|
||||
const change = convertToGoalChanges(person, type, value);
|
||||
const changes = pendingChanges.slice();
|
||||
if (index >= 0) {
|
||||
if (isNaN(value)) {
|
||||
changes.splice(index, 1); // remove it.
|
||||
} else {
|
||||
changes.splice(index, 1, change); // replace
|
||||
}
|
||||
} else {
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
changes.push(change); // add
|
||||
}
|
||||
setPendingChanges(changes);
|
||||
};
|
||||
|
||||
// todo, move to service.
|
||||
const convertToGoalChanges = (
|
||||
personId: string,
|
||||
type: Product2,
|
||||
value: number
|
||||
): IGoalChangeRequest => {
|
||||
let startDate = moment().utc().set("year", input.year);
|
||||
startDate = startDate.set("month", input.month - 1);
|
||||
startDate = startDate.clone().startOf("month");
|
||||
const endDate = startDate.clone();
|
||||
endDate.add(1, "month");
|
||||
endDate.subtract(1, "second");
|
||||
|
||||
return {
|
||||
departmentId: parseInt(input.departmentId, 10),
|
||||
personId: personId,
|
||||
product: type,
|
||||
value: value,
|
||||
startDate: startDate,
|
||||
endDate: endDate
|
||||
};
|
||||
};
|
||||
|
||||
const isValidChanges = (): boolean => {
|
||||
if (pendingChanges && pendingChanges.length > 0) {
|
||||
return GoalService.isValidPersonnelChanges(
|
||||
pendingChanges,
|
||||
filteredPersonnelGoals,
|
||||
departmentGoals.data
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
if (isValidChanges()) {
|
||||
GoalService.savePendingChanges(pendingChanges).then(() => {
|
||||
setLastSave("Changes Saved " + moment().format("LTS"));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submitDispersalViaExtrapolation = async (
|
||||
request: IGoalDispersalRequest
|
||||
) => {
|
||||
setDispersalReceipt({ state: AsyncState.Loading });
|
||||
const res = await GoalDispersalService.DisperseGoalsViaExtrapolation(
|
||||
request
|
||||
);
|
||||
setDispersalReceipt(res);
|
||||
if (res.state === AsyncState.Success) {
|
||||
dispatch(getPersonnelGoals(input));
|
||||
}
|
||||
};
|
||||
|
||||
const submitcustomWeighteDispersal = async (
|
||||
request: ICustomWeightedDispersalRequest
|
||||
) => {
|
||||
setDispersalReceipt({ state: AsyncState.Loading });
|
||||
const res = await GoalDispersalService.DisperseGoalsByCustomWeights(
|
||||
request
|
||||
);
|
||||
setDispersalReceipt(res);
|
||||
if (res.state === AsyncState.Success) {
|
||||
dispatch(getPersonnelGoals(input));
|
||||
}
|
||||
};
|
||||
|
||||
const exportCsv = () => {
|
||||
if (filteredPersonnelGoals && departmentGoals.data) {
|
||||
const personnelGoalsData = filteredPersonnelGoals;
|
||||
const departmentGoalsData = departmentGoals.data;
|
||||
|
||||
const productsWithPersonnelGoals = Product2.Products.filter(
|
||||
(product) => product.hasPersonnelGoals
|
||||
);
|
||||
|
||||
const deptGoals: MonthlyPersonnelGoalsCsvData = {
|
||||
rowHeader: "Department Goal",
|
||||
goals: productsWithPersonnelGoals.reduce(
|
||||
(perProductGoals, product) => ({
|
||||
...perProductGoals,
|
||||
[product.code]:
|
||||
departmentGoalsData.monthlyGoals[
|
||||
personnelGoalsData.month - 1
|
||||
].productGoals[product.code]
|
||||
}),
|
||||
{}
|
||||
)
|
||||
};
|
||||
|
||||
const goals: MonthlyPersonnelGoalsCsvData[] = personnelGoalsData.personnel.map(
|
||||
(person) => {
|
||||
return {
|
||||
rowHeader: person.fullName,
|
||||
goals: productsWithPersonnelGoals.reduce(
|
||||
(perProductGoals, product) => ({
|
||||
...perProductGoals,
|
||||
[product.code]:
|
||||
personnelGoalsData.goals[person.employeeId]
|
||||
.productGoals[product.code]
|
||||
}),
|
||||
{}
|
||||
)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const goalTotals: MonthlyPersonnelGoalsCsvData = {
|
||||
rowHeader: "Totals",
|
||||
goals: productsWithPersonnelGoals.reduce(
|
||||
(perProductGoals, product) => ({
|
||||
...perProductGoals,
|
||||
[product.code]: GoalService.sumPersonnelGoalsForProduct(
|
||||
personnelGoalsData,
|
||||
product
|
||||
)
|
||||
}),
|
||||
{}
|
||||
)
|
||||
};
|
||||
|
||||
ExportCsvService.exportMonthlyPersonnelGoalsAsCSV([
|
||||
deptGoals,
|
||||
...goals,
|
||||
goalTotals
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DispersalFormAlertBar
|
||||
goalDispersalReceiptContainer={dispersalReceipt}
|
||||
/>
|
||||
<PersonnelGoalsControls
|
||||
inputForm={input}
|
||||
onChange={(change) => setInput(change)}
|
||||
onSave={saveChanges}
|
||||
onExport={exportCsv}
|
||||
display={lastSave}
|
||||
departments={props.departments}
|
||||
allowSave={isValidChanges()}
|
||||
dispersalReceiptContainer={dispersalReceipt}
|
||||
submitDispersalViaExtrapolation={(request) =>
|
||||
submitDispersalViaExtrapolation(request)
|
||||
}
|
||||
submitCustomWeightedDispersal={(request) =>
|
||||
submitcustomWeighteDispersal(request)
|
||||
}
|
||||
/>
|
||||
{personnelGoals.state == AsyncState.Success &&
|
||||
personnelGoals.data &&
|
||||
filteredPersonnelGoals &&
|
||||
departmentGoals.state == AsyncState.Success &&
|
||||
departmentGoals.data ? (
|
||||
<TabView
|
||||
data-test="tabbed-commission-view"
|
||||
pages={[
|
||||
{
|
||||
header: PersonalGoalsTranslate.tabs.editor,
|
||||
element: (
|
||||
<PersonnelGoalsTable
|
||||
personnelGoals={filteredPersonnelGoals}
|
||||
departmentGoals={departmentGoals.data}
|
||||
pendingChanges={pendingChanges}
|
||||
onChange={queueChanges}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: PersonalGoalsTranslate.tabs.detail,
|
||||
element: <PersonnelGoalsDetail input={input} />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<div>Please Wait.... </div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonnelGoalsEditor;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import PersonnelGoalsEditor from "./PersonnelGoalsEditor";
|
||||
import { IDepartment } from "../../../shared/models/Department";
|
||||
import DepartmentService from "../../../shared/services/DepartmentService";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
const PersonnelGoalsPage: React.FC = () => {
|
||||
const [departments, setDepartments] = useState<IDepartment[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
DepartmentService.getAllDepartments(undefined, true)
|
||||
.then((response) => {
|
||||
setDepartments(response.data || []);
|
||||
})
|
||||
.catch(() => {
|
||||
setDepartments([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (departments && departments.length === 0) {
|
||||
return (
|
||||
<Alert color="warning" data-test="not-found">
|
||||
No departments found for your role
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return departments && departments.length > 0 ? (
|
||||
<PersonnelGoalsEditor departments={departments} />
|
||||
) : (
|
||||
<div>Please Wait.... </div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonnelGoalsPage;
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Product2 from "../../../shared/enumerations/Product";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IGoalChangeRequest,
|
||||
IPersonnelGoals
|
||||
} from "../../../shared/models/Goal";
|
||||
import PersonnelGoalsTable, { IProps } from "./PersonnelGoalsTable";
|
||||
import { IEmployee } from "../../../shared/models/Employee";
|
||||
|
||||
const person: IEmployee = {
|
||||
employeeId: "11771",
|
||||
adEmail: "[email protected]",
|
||||
personId: 0,
|
||||
fullName: "Test McTester",
|
||||
homeDepartment: "QA",
|
||||
baseRole: "QA",
|
||||
departments: [],
|
||||
role: {
|
||||
name: "Role1",
|
||||
description: "NONE",
|
||||
permissions: [{ code: "VIEW_DASHBOARD", description: "" }],
|
||||
hasCommissions: true
|
||||
},
|
||||
additionalPermissions: [],
|
||||
assignments: []
|
||||
};
|
||||
|
||||
describe("A table for editing personnel goals", () => {
|
||||
it("Displays a valid personnel goals edit table", () => {
|
||||
const personnelGoals: IPersonnelGoals = {
|
||||
year: 2021,
|
||||
personnel: [person],
|
||||
departmentId: 99,
|
||||
month: 1,
|
||||
goals: {
|
||||
"11771": {
|
||||
productGoals: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
const departmentGoals: IDepartmentGoals = {
|
||||
departmentId: 99,
|
||||
year: 2021,
|
||||
monthlyGoals: [
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} }
|
||||
]
|
||||
};
|
||||
const pendingChanges: IGoalChangeRequest[] = [];
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const onChange = () => {};
|
||||
let wrapper = shallow(
|
||||
<PersonnelGoalsTable
|
||||
personnelGoals={personnelGoals}
|
||||
departmentGoals={departmentGoals}
|
||||
onChange={onChange}
|
||||
pendingChanges={pendingChanges}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find("[id='personnel-goals-table']").length).toBe(1);
|
||||
});
|
||||
|
||||
describe("Products with personnel goals", () => {
|
||||
it("Displays titles in the table header for products that have personnel goals", () => {
|
||||
const wrapper = renderPersonnelGoalsTable();
|
||||
const expectedProductNames = productWithPersonnelGoals().map(
|
||||
(product) => product.title
|
||||
);
|
||||
const headerCols = wrapper.find("thead > tr > th").slice(1);
|
||||
|
||||
expect(headerCols.length).toEqual(
|
||||
productWithPersonnelGoals().length
|
||||
);
|
||||
headerCols.forEach((col) =>
|
||||
expect(expectedProductNames).toContain(col.text())
|
||||
);
|
||||
});
|
||||
|
||||
it("Match titles in the department goal row to products that have personnel goals ", () => {
|
||||
const wrapper = renderPersonnelGoalsTable();
|
||||
const headerCols = wrapper
|
||||
.find("thead > tr.alert-info > td")
|
||||
.slice(1);
|
||||
const expectedProductCodes = productWithPersonnelGoals().map(
|
||||
(product) => product.code
|
||||
);
|
||||
|
||||
expect(headerCols.length).toEqual(
|
||||
productWithPersonnelGoals().length
|
||||
);
|
||||
|
||||
headerCols.forEach((col, index) =>
|
||||
expect(
|
||||
col
|
||||
.prop("title")
|
||||
?.startsWith(`total_${expectedProductCodes[index]}`)
|
||||
).toBe(true)
|
||||
);
|
||||
});
|
||||
|
||||
it("Displays only products with personnel goals in each GoalEditCell", () => {
|
||||
const wrapper = renderPersonnelGoalsTable();
|
||||
const goalEditCells = wrapper.find("tbody GoalEditCell");
|
||||
|
||||
expect(goalEditCells.length).toEqual(
|
||||
productWithPersonnelGoals().length
|
||||
);
|
||||
|
||||
goalEditCells.forEach((cell) => {
|
||||
const goal: { type: Product2 } = cell.prop("goal");
|
||||
expect(goal.type.hasPersonnelGoals).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("Displays only product with personnel goals in each MonthlyProductTotal cell", () => {
|
||||
const wrapper = renderPersonnelGoalsTable();
|
||||
const productsInFooter: Product2[] = wrapper
|
||||
.find("tfoot MonthlyProductTotal")
|
||||
.map((cell) => cell.prop("product"));
|
||||
|
||||
expect(productsInFooter.length).toEqual(
|
||||
productWithPersonnelGoals().length
|
||||
);
|
||||
productsInFooter.forEach((product) =>
|
||||
expect(product.hasPersonnelGoals).toBe(true)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderPersonnelGoalsTable(args = {}) {
|
||||
const personnelGoals: IPersonnelGoals = {
|
||||
year: 2021,
|
||||
personnel: [person],
|
||||
departmentId: 99,
|
||||
month: 1,
|
||||
goals: {
|
||||
"11771": {
|
||||
productGoals: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
const departmentGoals: IDepartmentGoals = {
|
||||
departmentId: 99,
|
||||
year: 2021,
|
||||
monthlyGoals: [
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} },
|
||||
{ productGoals: {} }
|
||||
]
|
||||
};
|
||||
|
||||
const defaultProps: IProps = {
|
||||
personnelGoals,
|
||||
departmentGoals,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onChange: () => {},
|
||||
pendingChanges: []
|
||||
};
|
||||
|
||||
const props = { ...defaultProps, ...args };
|
||||
return shallow(<PersonnelGoalsTable {...props} />);
|
||||
}
|
||||
|
||||
function productWithPersonnelGoals() {
|
||||
return Product2.Products.filter((product) => product.hasPersonnelGoals);
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IDepartmentGoals,
|
||||
IGoalChangeRequest,
|
||||
IPersonnelGoals
|
||||
} from "../../../shared/models/Goal";
|
||||
import { Product2 } from "../../../shared/enumerations/Product";
|
||||
import GoalEditCell from "../GoalEditCell";
|
||||
import MonthlyProductTotal from "./MonthlyProductTotal";
|
||||
import GoalService from "../../../shared/services/GoalService";
|
||||
import UserDisplay from "../../shared/UserDisplay";
|
||||
|
||||
export interface IProps {
|
||||
personnelGoals: IPersonnelGoals;
|
||||
departmentGoals: IDepartmentGoals;
|
||||
pendingChanges: IGoalChangeRequest[];
|
||||
onChange: (label: string, type: Product2, value: number) => void;
|
||||
}
|
||||
|
||||
const PersonnelGoalsTable: React.FC<IProps> = (props: IProps) => {
|
||||
const onAction = (label: string, type: Product2, value: number) => {
|
||||
props.onChange(label, type, value);
|
||||
};
|
||||
|
||||
return (
|
||||
<table
|
||||
className="table table-striped tablecenterCSS tablecenterheadCSS"
|
||||
id="personnel-goals-table"
|
||||
>
|
||||
<thead className="thead-light">
|
||||
<tr>
|
||||
<th scope="col">Personnel</th>
|
||||
{GoalService.productsWithPersonnelGoals().map((product) => (
|
||||
<th key={product.code} scope="col" title={product.code}>
|
||||
{product.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
<tr className="alert alert-info">
|
||||
<td>Department Goal</td>
|
||||
{GoalService.productsWithPersonnelGoals().map((product) => (
|
||||
<td
|
||||
key={"total_" + product.code}
|
||||
scope="col"
|
||||
title={
|
||||
"total_" +
|
||||
product.code +
|
||||
"_" +
|
||||
props.personnelGoals.month
|
||||
}
|
||||
>
|
||||
{GoalService.formatMonthlyGoal(
|
||||
GoalService.getMonthlyGoal(
|
||||
props.departmentGoals,
|
||||
props.personnelGoals.month,
|
||||
product.code
|
||||
),
|
||||
product
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
<tr className="alert alert-secondary">
|
||||
<td>Totals</td>
|
||||
{GoalService.productsWithPersonnelGoals().map((product) => (
|
||||
<td key={"total_" + product.code} scope="col">
|
||||
<MonthlyProductTotal
|
||||
personnelGoals={props.personnelGoals}
|
||||
pendingChanges={props.pendingChanges}
|
||||
product={product}
|
||||
departmentTotal={GoalService.getMonthlyGoal(
|
||||
props.departmentGoals,
|
||||
props.personnelGoals.month,
|
||||
product.code
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="form-group">
|
||||
{props.personnelGoals.personnel.map((person, index: number) => (
|
||||
<tr key={"personnelGoalsRow_" + index}>
|
||||
<td
|
||||
title={person.employeeId + " - " + person.adEmail}
|
||||
className="tdLabel"
|
||||
>
|
||||
<UserDisplay employeeId={person.employeeId} />
|
||||
</td>
|
||||
{GoalService.productsWithPersonnelGoals().map(
|
||||
(product, index2) => (
|
||||
<td
|
||||
key={person + "_" + product.code}
|
||||
scope="col"
|
||||
title="figure me out"
|
||||
>
|
||||
<GoalEditCell
|
||||
goal={{
|
||||
year: props.personnelGoals.year,
|
||||
month: props.personnelGoals.month,
|
||||
type: product,
|
||||
value:
|
||||
props.personnelGoals.goals[
|
||||
person.employeeId
|
||||
].productGoals[product.code],
|
||||
departmentId:
|
||||
props.personnelGoals
|
||||
.departmentId,
|
||||
personId: person.employeeId
|
||||
}}
|
||||
tabIndex={index + (index2 + 1) * 100}
|
||||
requestAction={(
|
||||
type: Product2,
|
||||
value: number
|
||||
) =>
|
||||
onAction(
|
||||
person.employeeId,
|
||||
type,
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="alert alert-secondary">
|
||||
<td>Totals</td>
|
||||
{GoalService.productsWithPersonnelGoals().map((product) => (
|
||||
<td key={"total_" + product.code} scope="col">
|
||||
<MonthlyProductTotal
|
||||
personnelGoals={props.personnelGoals}
|
||||
pendingChanges={props.pendingChanges}
|
||||
product={product}
|
||||
departmentTotal={GoalService.getMonthlyGoal(
|
||||
props.departmentGoals,
|
||||
props.personnelGoals.month,
|
||||
product.code
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonnelGoalsTable;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { AutoTable } from "../../components/auto-table/AutoTable";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import { IBusinessLine } from "../../shared/models/Settings";
|
||||
export interface BusinessLineTableProps {
|
||||
businessLines: IBusinessLine[];
|
||||
}
|
||||
|
||||
const BusinessLineTable: React.FC<BusinessLineTableProps> = (props) => {
|
||||
const headers = [
|
||||
Translate.Get.containers.businessLineTable.headers.code,
|
||||
Translate.Get.containers.businessLineTable.headers.name,
|
||||
Translate.Get.containers.businessLineTable.headers.orgCode
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IBusinessLine) => x.code,
|
||||
(x: IBusinessLine) => x.name,
|
||||
(x: IBusinessLine) => x.orgCode
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-2">
|
||||
<h5>{Translate.Get.containers.businessLineTable.title}</h5>
|
||||
<AutoTable
|
||||
className="border rounded"
|
||||
cells={cells}
|
||||
headers={headers}
|
||||
elements={props.businessLines}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessLineTable;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { AutoTable } from "../../components/auto-table/AutoTable";
|
||||
import Translate from "./../../translate/Translate";
|
||||
|
||||
export interface OrganizationTableProps {
|
||||
organizations: string[][];
|
||||
}
|
||||
|
||||
const OrganizationTable: React.FC<OrganizationTableProps> = (props) => {
|
||||
const headers = [
|
||||
Translate.Get.containers.organizationTable.headers.orgCode,
|
||||
Translate.Get.containers.organizationTable.headers.orgName
|
||||
];
|
||||
|
||||
const cells = [(x: any[]) => x[0], (x: any[]) => x[1]];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-2">
|
||||
<h5>{Translate.Get.containers.organizationTable.title}</h5>
|
||||
<AutoTable
|
||||
className="border rounded"
|
||||
cells={cells}
|
||||
headers={headers}
|
||||
elements={props.organizations}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationTable;
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { flattenRecord } from "../../shared/services/SettingsService";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import OrganizationTable from "./OrganizationTable";
|
||||
import BusinessLineTable from "./BusinessLineTable";
|
||||
import { IDepartmentState } from "./../../reducers/DepartmentReducer";
|
||||
import {
|
||||
getAllDepartments,
|
||||
updateExistingDepartment,
|
||||
submitNewDepartment
|
||||
} from "../../actions/DepartmentActions";
|
||||
import { getAllUserAssignments } from "../../actions/UserAssignmentActions";
|
||||
import DepartmentTable from "../../components/department-table/DepartmentTable";
|
||||
import { useDispatch } from "react-redux";
|
||||
import RegionTable from "../../components/region/RegionTable";
|
||||
import { IUserAssignmentState } from "../../reducers/UserAssignmentReducer";
|
||||
import UserAssignmentService from "../../shared/services/UserAssignmentService";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import { profileSearch } from "../../actions/ProfileActions";
|
||||
import { IUserAssignmentDto } from "../../shared/models/UserAssignment";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
|
||||
type OrganizationViewProps = RouteComponentProps;
|
||||
|
||||
const OrganizationView: React.FC<OrganizationViewProps> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const settings = React.useContext(SettingsContext);
|
||||
|
||||
// In this case we still load departments instead of using settings because
|
||||
// the appUser may make modifications. It might be smarter to have the appUser
|
||||
// reload. They'll almost never 'create' a department.
|
||||
const departmentState = useSelector<IAppState, IDepartmentState>(
|
||||
(state) => state.departmentState
|
||||
);
|
||||
|
||||
const userAssignmentState = useSelector<IAppState, IUserAssignmentState>(
|
||||
(state) => state.userAssignmentState
|
||||
);
|
||||
|
||||
const profileState = useSelector<IAppState, IProfileState>(
|
||||
(state) => state.profileState
|
||||
);
|
||||
|
||||
const [organizations, setOrganizations] = useState<string[][]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllDepartments());
|
||||
dispatch(getAllUserAssignments());
|
||||
dispatch(profileSearch(null, "RegionalManager"));
|
||||
}, []);
|
||||
|
||||
// TODO, Handle department creation => Department refresh
|
||||
useEffect(() => {
|
||||
dispatch(getAllDepartments());
|
||||
}, [departmentState.submitState.data]);
|
||||
|
||||
useEffect(() => {
|
||||
setOrganizations(flattenRecord(settings?.organizationSet));
|
||||
}, [settings]);
|
||||
|
||||
const [
|
||||
submitRegionalManagerUpdateState,
|
||||
setSubmitRegionalManagerUpdateState
|
||||
] = useState<IAsyncStateContainer<string>>({ state: AsyncState.Inactive });
|
||||
|
||||
const regionalManagerAssignments = UserAssignmentService.filterUserAssignmentsByType(
|
||||
userAssignmentState.userAssignments.data ?? [],
|
||||
"RegionalManager"
|
||||
);
|
||||
|
||||
const updateRegionalManager = async (
|
||||
employeeId: string,
|
||||
regionCode: string
|
||||
) => {
|
||||
setSubmitRegionalManagerUpdateState({ state: AsyncState.Loading });
|
||||
const assignment: IUserAssignmentDto = {
|
||||
employeeId: employeeId,
|
||||
key: regionCode
|
||||
};
|
||||
const result = await UserAssignmentService.setRegionalManager(
|
||||
assignment
|
||||
);
|
||||
setSubmitRegionalManagerUpdateState(result);
|
||||
dispatch(getAllUserAssignments());
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{Translate.Get.containers.organizationView.pageTitle}</h3>
|
||||
<OrganizationTable organizations={organizations} />
|
||||
<BusinessLineTable businessLines={settings?.businessLines ?? []} />
|
||||
<RegionTable
|
||||
assignedManagers={regionalManagerAssignments}
|
||||
availableManagers={profileState.searchResults.data ?? []}
|
||||
regions={settings?.regions ?? []}
|
||||
updateManager={updateRegionalManager}
|
||||
submitState={submitRegionalManagerUpdateState}
|
||||
/>
|
||||
<DepartmentTable
|
||||
departments={departmentState.departments.data ?? []}
|
||||
organizationSet={settings?.organizationSet ?? {}}
|
||||
departmentStateSet={settings?.departmentStateSet ?? {}}
|
||||
businessLines={settings?.businessLines ?? []}
|
||||
regions={settings?.regions ?? []}
|
||||
updateDepartment={(updatedData, id) => {
|
||||
dispatch(updateExistingDepartment(id, updatedData));
|
||||
}}
|
||||
submitNewDepartment={(newDepartment) => {
|
||||
dispatch(submitNewDepartment(newDepartment));
|
||||
}}
|
||||
submitState={departmentState.submitState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationView;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
import DepartmentDisplay from "../shared/DepartmentDisplay";
|
||||
import PayoutProfileForRole from "./PayoutProfileForRole";
|
||||
import DepartmentService from "../../shared/services/DepartmentService";
|
||||
import CommissionsForRole from "../commissions-page/CommissionsForRole";
|
||||
|
||||
export interface IPayoutProfileProps {
|
||||
profile: IEmployee;
|
||||
}
|
||||
|
||||
const EligiblePayouts: React.FC<IPayoutProfileProps> = (props) => {
|
||||
const profile = props.profile;
|
||||
const activeDepartments = profile.departments.filter((userDept) =>
|
||||
DepartmentService.isActiveAssignment(userDept)
|
||||
);
|
||||
|
||||
return activeDepartments.length > 0 ? (
|
||||
<span>
|
||||
{activeDepartments.map((department, idx) => {
|
||||
return (
|
||||
<div
|
||||
key={idx + "_payoutProfile_" + department.departmentId}
|
||||
>
|
||||
<h5>
|
||||
<DepartmentDisplay
|
||||
departmentId={department.departmentId}
|
||||
/>
|
||||
: You get payouts & commissions for{" "}
|
||||
{department.role?.name || "Missing"}
|
||||
</h5>
|
||||
{/*<CommissionProfileForRole role={profile.role}/>*/}
|
||||
<PayoutProfileForRole role={department.role} />
|
||||
<CommissionsForRole role={department.role} />
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
) : (
|
||||
<span>No eligible payouts.</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default EligiblePayouts;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import PayoutProfileCard from "../../components/payouts/PayoutProfileCard";
|
||||
import { IAppRole, IPayoutProfile } from "../../shared/models/Settings";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import { CardDeck } from "reactstrap";
|
||||
import "./PayoutProfilePage.scss";
|
||||
|
||||
export interface IPayoutProfileProps {
|
||||
role: IAppRole | undefined;
|
||||
}
|
||||
|
||||
const PayoutProfileForRole: React.FC<IPayoutProfileProps> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const payoutProfiles = settings?.payoutProfiles;
|
||||
let profiles: IPayoutProfile[] = [];
|
||||
|
||||
if (payoutProfiles) {
|
||||
profiles = payoutProfiles.filter((payoutProfile) =>
|
||||
payoutProfile.roles.find((x) => x.name == props.role?.name)
|
||||
);
|
||||
}
|
||||
|
||||
return profiles.length > 0 && props.role ? (
|
||||
<CardDeck className={"FlexCenter"}>
|
||||
{profiles?.map((payoutProfile, idx) => {
|
||||
return (
|
||||
<PayoutProfileCard
|
||||
profile={payoutProfile}
|
||||
showRoles
|
||||
key={"payoutProfileCard_" + idx}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CardDeck>
|
||||
) : (
|
||||
<span>No eligible goal based payouts.</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayoutProfileForRole;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.FittedCard {
|
||||
width: 30%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.FlexCenter {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import PayoutProfileCard from "../../components/payouts/PayoutProfileCard";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import { CardDeck } from "reactstrap";
|
||||
import "./PayoutProfilePage.scss";
|
||||
|
||||
const PayoutProfilePage: React.FC = () => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
|
||||
const payoutProfiles = settings?.payoutProfiles;
|
||||
|
||||
return payoutProfiles ? (
|
||||
<div>
|
||||
<h4>{Translate.Get.containers.payoutProfileView.pageTitle}</h4>
|
||||
<CardDeck className={"FlexCenter"}>
|
||||
{payoutProfiles
|
||||
?.sort((a, b) => a.payouts.size)
|
||||
.map((payoutProfile, idx) => {
|
||||
return (
|
||||
<div className={"FittedCard"}>
|
||||
<PayoutProfileCard
|
||||
profile={payoutProfile}
|
||||
showRoles
|
||||
key={"payoutProfileCard_" + idx}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardDeck>
|
||||
</div>
|
||||
) : (
|
||||
<h3>Could not load payout profiles.</h3>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayoutProfilePage;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import ExportPayoutPage from "./ExportPayoutPage";
|
||||
|
||||
describe("A Component For exporting payouts", () => {
|
||||
it("Displays the export options", () => {
|
||||
const wrapper = shallow(<ExportPayoutPage />);
|
||||
const tableData = wrapper.find("[data-test='export-payout-page']");
|
||||
expect(tableData.length).toBe(1);
|
||||
|
||||
const buttons = wrapper.find("button");
|
||||
expect(buttons.length).toBe(2);
|
||||
|
||||
const selects = wrapper.find("select");
|
||||
expect(selects.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import React, { useState } from "react";
|
||||
import moment from "moment";
|
||||
import GoalService from "../../shared/services/GoalService";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { Alert } from "reactstrap";
|
||||
import PayoutService from "../../shared/services/PayoutService";
|
||||
import { IAccountingSummary, IPayoutEntry } from "../../shared/models/Payout";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import AsyncGuard from "../shared/AsyncGuard";
|
||||
import PayoutEntryTable from "../../components/payouts/PayoutEntryTable";
|
||||
import ExportCsvService from "../../shared/services/ExportCsvService";
|
||||
|
||||
const ExportPayoutPage: React.FC = () => {
|
||||
const [month, setMonth] = useState<number>(
|
||||
moment().add(1, "months").month()
|
||||
);
|
||||
const [year, setYear] = useState<number>(moment().year());
|
||||
const [payouts, setPayouts] = useState<
|
||||
IAsyncStateContainer<IPayoutEntry[]>
|
||||
>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
const [accountingReport, setAccountingReport] = useState<
|
||||
IAsyncStateContainer<IAccountingSummary>
|
||||
>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
|
||||
const years = [moment().year() - 2, moment().year() - 1, moment().year()];
|
||||
|
||||
const submitForm = async () => {
|
||||
await fetchPayoutData();
|
||||
};
|
||||
|
||||
const submitAccountingForm = async () => {
|
||||
setAccountingReport({
|
||||
state: AsyncState.Loading
|
||||
});
|
||||
const date = moment({ year: year, month: month - 1, day: 1 });
|
||||
const response = await PayoutService.GetAccountingReport(date);
|
||||
if (response.data) {
|
||||
setAccountingReport(response);
|
||||
ExportCsvService.exportAccountingReportCSV(response.data);
|
||||
} else {
|
||||
setAccountingReport({
|
||||
state: AsyncState.NotFound
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPayoutData = async () => {
|
||||
setPayouts({
|
||||
state: AsyncState.Loading
|
||||
});
|
||||
const response = await PayoutService.GetExportPayouts(year, month);
|
||||
setPayouts(response);
|
||||
setAccountingReport({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
};
|
||||
|
||||
const exportCsv = () => {
|
||||
if (payouts.data && payouts.data.length > 0) {
|
||||
ExportCsvService.exportDayforcePayoutCSV(payouts.data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test="export-payout-page">
|
||||
<Alert color="info">
|
||||
{Translate.Get.containers.exportPayoutPage.instructions}
|
||||
</Alert>
|
||||
<select
|
||||
className="mx-1"
|
||||
id="fromMonth"
|
||||
value={month}
|
||||
onChange={(event) => {
|
||||
setMonth(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
{GoalService.Months.map(
|
||||
(monthStr: string, monthNum: number) => (
|
||||
<option key={monthNum} value={monthNum + 1}>
|
||||
{monthStr}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
<select
|
||||
className="mx-1"
|
||||
id="fromYear"
|
||||
value={year}
|
||||
onChange={(event) => {
|
||||
setYear(parseInt(event.target.value, 10));
|
||||
}}
|
||||
>
|
||||
{years.map((year: number) => (
|
||||
<option key={year} value={year}>
|
||||
{year}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-outline-info btn-sm mx-2"
|
||||
type="button"
|
||||
onClick={submitForm}
|
||||
>
|
||||
{Translate.Get.containers.exportPayoutPage.exportButton}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-info btn-sm mx-2"
|
||||
type="button"
|
||||
onClick={submitAccountingForm}
|
||||
>
|
||||
{
|
||||
Translate.Get.containers.exportPayoutPage
|
||||
.exportAccountingButton
|
||||
}
|
||||
</button>
|
||||
<AsyncGuard container={payouts}>
|
||||
<>
|
||||
{payouts.data && payouts.data.length > 0 ? (
|
||||
<button
|
||||
className="btn btn-secondary btn-sm"
|
||||
onClick={exportCsv}
|
||||
>
|
||||
{" "}
|
||||
{
|
||||
Translate.Get.containers.exportPayoutPage
|
||||
.downloadButton
|
||||
}
|
||||
</button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</AsyncGuard>
|
||||
<br />
|
||||
<AsyncGuard container={accountingReport}>
|
||||
<Alert color="info" className="mt-2">
|
||||
Accounting Report Exported
|
||||
</Alert>
|
||||
</AsyncGuard>
|
||||
<AsyncGuard container={payouts}>
|
||||
<PayoutEntryTable payoutEntries={payouts.data || []} />
|
||||
</AsyncGuard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportPayoutPage;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import PayoutPage from "./PayoutPage";
|
||||
import PayoutService from "../../shared/services/PayoutService";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { IPayoutLineItem } from "../../shared/models/Payout";
|
||||
|
||||
describe("A Component For displaying an employee payout", () => {
|
||||
const payoutServiceMock = jest.spyOn(PayoutService, "getEmployeePayouts");
|
||||
|
||||
beforeEach(() => {
|
||||
payoutServiceMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays loading wait", () => {
|
||||
payoutServiceMock.mockResolvedValue({
|
||||
state: AsyncState.Loading
|
||||
} as IAsyncStateContainer<IPayoutLineItem[]>);
|
||||
|
||||
const wrapper = shallow(<PayoutPage employeeId="11771" />);
|
||||
expect(wrapper.html()).toBe("Loading...");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import PayoutView from "../../components/payouts/PayoutView";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import moment, { Moment } from "moment";
|
||||
import { CustomInput, Form, Input } from "reactstrap";
|
||||
import PayoutService from "../../shared/services/PayoutService";
|
||||
import { IPayoutLineItem } from "../../shared/models/Payout";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
|
||||
export interface IEmployeeParam {
|
||||
employeeId: string;
|
||||
}
|
||||
|
||||
const PayoutPage: React.FC<IEmployeeParam> = (props) => {
|
||||
const employeeId = props.employeeId;
|
||||
const [fromDate, setFromDate] = useState<Moment>(moment().startOf("month"));
|
||||
const [showDetails, setShowDetails] = useState<boolean>(false);
|
||||
const [lineItems, setLineItems] = useState<
|
||||
IAsyncStateContainer<IPayoutLineItem[]>
|
||||
>({
|
||||
state: AsyncState.Loading
|
||||
});
|
||||
const settings = React.useContext(SettingsContext);
|
||||
|
||||
useEffect(() => {
|
||||
setLineItems({
|
||||
state: AsyncState.Loading
|
||||
});
|
||||
const fetchData = async () => {
|
||||
return await PayoutService.getEmployeePayouts({
|
||||
employeeId: employeeId,
|
||||
year: fromDate.year(),
|
||||
month: fromDate.month() + 1
|
||||
});
|
||||
};
|
||||
fetchData().then((lineItems) => {
|
||||
setLineItems(lineItems);
|
||||
});
|
||||
}, [employeeId, fromDate]);
|
||||
|
||||
return lineItems.state === AsyncState.Success && lineItems.data ? (
|
||||
<>
|
||||
<div className="d-flex justify-content-end m-2">
|
||||
Next Incentive Payout Date:{" "}
|
||||
{moment(settings?.nextBonusPeriod).format("MM/DD/yyyy")}
|
||||
</div>
|
||||
<div className="d-flex justify-content-end">
|
||||
<Form inline noValidate>
|
||||
Payout details for
|
||||
<Input
|
||||
name="AcceptedStateDateTime"
|
||||
type="month"
|
||||
bsSize="sm"
|
||||
required
|
||||
className="mb-1 ml-2"
|
||||
value={fromDate.format("YYYY-MM")}
|
||||
onChange={(event) => {
|
||||
setFromDate(
|
||||
moment(event.target.value).startOf("month")
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<CustomInput
|
||||
type="checkbox"
|
||||
id="toggle-all-activity"
|
||||
checked={showDetails}
|
||||
onChange={(event) => {
|
||||
setShowDetails(event.target.checked);
|
||||
}}
|
||||
label="Show Details"
|
||||
className="ml-4 mr-1"
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<PayoutView
|
||||
userPayouts={lineItems.data}
|
||||
showDetails={showDetails}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>Loading...</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayoutPage;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import ProfilePage from "./ProfilePage";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
import ProfileView from "../../components/profile/ProfileView";
|
||||
|
||||
jest.mock("react-redux");
|
||||
|
||||
describe("A component for displaying appUser profile", () => {
|
||||
it("Displays a valid profile", () => {
|
||||
const profile: IEmployee = {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
additionalPermissions: [],
|
||||
role: {
|
||||
name: "Role1",
|
||||
description: "NONE",
|
||||
permissions: [{ code: "VIEW_DASHBOARD", description: "" }],
|
||||
hasCommissions: true
|
||||
},
|
||||
departments: [],
|
||||
assignments: []
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const refreshProfile = () => {};
|
||||
|
||||
let wrapper = shallow(
|
||||
<ProfilePage profile={profile} refreshProfile={refreshProfile} />
|
||||
);
|
||||
|
||||
const found = wrapper.find(ProfileView);
|
||||
expect(found.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ProfileView from "../../components/profile/ProfileView";
|
||||
import { Alert, Col, ListGroup, ListGroupItem, Row } from "reactstrap";
|
||||
import EmployeePage from "../employee-page/EmployeePage";
|
||||
import PermissionsPage from "../../components/permission/PermissionsPage";
|
||||
import PayoutPage from "../payouts/PayoutPage";
|
||||
import {
|
||||
IEmployee,
|
||||
IEmployeeDetailsUpdate
|
||||
} from "../../shared/models/Employee";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import PermissionGuard from "../shared/PermissionGuard";
|
||||
import EmployeeStatusGuard from "../shared/EmployeeStatusGuard";
|
||||
import moment from "moment";
|
||||
import EligiblePayouts from "../payout-profile/EligiblePayouts";
|
||||
import EmployeeDetailsPage from "../employee-page/EmployeeDetailsPage";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
|
||||
interface IProfileParams {
|
||||
profile: IEmployee;
|
||||
employeeDetailsUpdateResult: string;
|
||||
submitStatus: IAsyncStateContainer<string>;
|
||||
refreshProfile: () => void;
|
||||
submitEmployeeDetailsUpdate: (update: IEmployeeDetailsUpdate) => void;
|
||||
}
|
||||
|
||||
const ProfilePage: React.FC<IProfileParams> = (props) => {
|
||||
const [activeSheet, setActiveSheet] = useState<string>("profile");
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const profile = props.profile;
|
||||
const name = profile.fullName || "Unknown";
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col xs="3">
|
||||
<ListGroup>
|
||||
<ListGroupItem
|
||||
active={activeSheet === "profile"}
|
||||
tag="button"
|
||||
onClick={() => {
|
||||
setActiveSheet("profile");
|
||||
}}
|
||||
action
|
||||
>
|
||||
Profile
|
||||
</ListGroupItem>
|
||||
<ListGroupItem
|
||||
active={activeSheet === "eligibility"}
|
||||
tag="button"
|
||||
onClick={() => {
|
||||
setActiveSheet("eligibility");
|
||||
}}
|
||||
action
|
||||
>
|
||||
Payout Eligibility
|
||||
</ListGroupItem>
|
||||
<ListGroupItem
|
||||
active={activeSheet === "payouts"}
|
||||
tag="button"
|
||||
onClick={() => {
|
||||
setActiveSheet("payouts");
|
||||
}}
|
||||
action
|
||||
>
|
||||
Payouts
|
||||
</ListGroupItem>
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
"ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
]}
|
||||
>
|
||||
<EmployeeStatusGuard employee={profile}>
|
||||
<ListGroupItem
|
||||
active={activeSheet === "departments"}
|
||||
tag="button"
|
||||
onClick={() => {
|
||||
setActiveSheet("departments");
|
||||
}}
|
||||
action
|
||||
>
|
||||
Manage Departments
|
||||
</ListGroupItem>
|
||||
</EmployeeStatusGuard>
|
||||
</PermissionGuard>
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
"ASSIGN_PERMISSIONS_TO_ASSOCIATES"
|
||||
]}
|
||||
>
|
||||
<EmployeeStatusGuard employee={profile}>
|
||||
<ListGroupItem
|
||||
active={activeSheet === "permissions"}
|
||||
tag="button"
|
||||
onClick={() => {
|
||||
setActiveSheet("permissions");
|
||||
}}
|
||||
action
|
||||
>
|
||||
Manage Permissions
|
||||
</ListGroupItem>
|
||||
</EmployeeStatusGuard>
|
||||
</PermissionGuard>
|
||||
<PermissionGuard
|
||||
requiredPermissions={["MANAGE_EMPLOYEE_DETAILS"]}
|
||||
>
|
||||
<EmployeeStatusGuard employee={profile}>
|
||||
<ListGroupItem
|
||||
active={activeSheet === "employeeDetails"}
|
||||
tag="button"
|
||||
onClick={() => {
|
||||
setActiveSheet("employeeDetails");
|
||||
}}
|
||||
action
|
||||
>
|
||||
Manage Employee Details
|
||||
</ListGroupItem>
|
||||
</EmployeeStatusGuard>
|
||||
</PermissionGuard>
|
||||
</ListGroup>
|
||||
<br />
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<h3>{name}</h3>
|
||||
{profile.terminated ? (
|
||||
<Alert color="warning">
|
||||
Employee Not Active As Of{" "}
|
||||
{moment(profile.terminated).format("MM/DD/YYYY")}
|
||||
</Alert>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{(() => {
|
||||
switch (activeSheet) {
|
||||
case "departments":
|
||||
return (
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
"ASSIGN_ASSOCIATES_TO_DEPARTMENT"
|
||||
]}
|
||||
>
|
||||
<EmployeePage
|
||||
employeeId={profile.employeeId}
|
||||
/>
|
||||
</PermissionGuard>
|
||||
);
|
||||
case "permissions":
|
||||
return (
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
"ASSIGN_PERMISSIONS_TO_ASSOCIATES"
|
||||
]}
|
||||
>
|
||||
<PermissionsPage
|
||||
additionalPermissions={
|
||||
profile.additionalPermissions ?? []
|
||||
}
|
||||
rolePermissions={
|
||||
profile.role.permissions
|
||||
}
|
||||
allPermissions={
|
||||
settings?.permissions ?? []
|
||||
}
|
||||
refreshProfile={() => {
|
||||
props.refreshProfile();
|
||||
}}
|
||||
employeeId={profile.employeeId}
|
||||
/>
|
||||
</PermissionGuard>
|
||||
);
|
||||
case "employeeDetails":
|
||||
return (
|
||||
<PermissionGuard
|
||||
requiredPermissions={[
|
||||
"MANAGE_EMPLOYEE_DETAILS"
|
||||
]}
|
||||
>
|
||||
<EmployeeDetailsPage
|
||||
profile={profile}
|
||||
employeeDetailsUpdateResult={
|
||||
props.employeeDetailsUpdateResult
|
||||
}
|
||||
submitStatus={props.submitStatus}
|
||||
refreshProfile={() => {
|
||||
props.refreshProfile();
|
||||
}}
|
||||
submitEmployeeDetailsUpdate={
|
||||
props.submitEmployeeDetailsUpdate
|
||||
}
|
||||
/>
|
||||
</PermissionGuard>
|
||||
);
|
||||
case "payouts":
|
||||
return (
|
||||
<PayoutPage employeeId={profile.employeeId} />
|
||||
);
|
||||
case "eligibility":
|
||||
return <EligiblePayouts profile={profile} />;
|
||||
case "profile":
|
||||
default:
|
||||
return <ProfileView profile={profile} />;
|
||||
}
|
||||
})()}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { AutoTable } from "../../components/auto-table/AutoTable";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { IEmployeeSummary } from "../../shared/models/Employee";
|
||||
|
||||
export interface IProfileTableProps {
|
||||
profiles: IEmployeeSummary[];
|
||||
}
|
||||
|
||||
const ProfileResultsTable: React.FC<IProfileTableProps> = (props) => {
|
||||
const history = useHistory();
|
||||
|
||||
const headers = [
|
||||
Translate.Get.containers.profileResultsTable.headers.name,
|
||||
Translate.Get.containers.profileResultsTable.headers.id,
|
||||
Translate.Get.containers.profileResultsTable.headers.email
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IEmployeeSummary) => x.fullName,
|
||||
(x: IEmployeeSummary) => x.employeeId,
|
||||
(x: IEmployeeSummary) => x.adEmail
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-2">
|
||||
<h5>{Translate.Get.containers.profileResultsTable.title}</h5>
|
||||
<AutoTable
|
||||
className="border rounded"
|
||||
cells={cells}
|
||||
headers={headers}
|
||||
elements={props.profiles}
|
||||
onRowClick={(selection: IEmployeeSummary) => {
|
||||
history.push("/profile/" + selection.employeeId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileResultsTable;
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import ProfileSearchPage from "./ProfileSearchPage";
|
||||
|
||||
describe("A component for finding employee profiles", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a blank search form", () => {
|
||||
const profile: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Role1",
|
||||
description: "NONE",
|
||||
permissions: [
|
||||
{ code: "VIEW_DASHBOARD", description: "" }
|
||||
],
|
||||
hasCommissions: true
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [],
|
||||
assignments: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(profile);
|
||||
let wrapper = mount(<ProfileSearchPage />);
|
||||
|
||||
const found = wrapper.find("[data-test='profile-search-page']");
|
||||
expect(found.length).toBe(1);
|
||||
|
||||
expect(dummyDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { profileSearch } from "../../actions/ProfileActions";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { FormGroup, Input, Label } from "reactstrap";
|
||||
import { debounce } from "debounce";
|
||||
import ProfileResultsTable from "./ProfileResultsTable";
|
||||
|
||||
const profileTranslate = Translate.Get.components.profileSearch;
|
||||
|
||||
const ProfileSearchPage: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (searchQuery && searchQuery.length > 1) {
|
||||
search(searchQuery);
|
||||
}
|
||||
}, [searchQuery]);
|
||||
|
||||
const search = useCallback(
|
||||
debounce((query: string) => {
|
||||
dispatch(profileSearch(query));
|
||||
}, 500),
|
||||
[]
|
||||
);
|
||||
|
||||
const profileState = useSelector<IAppState, IProfileState>(
|
||||
(state) => state.profileState
|
||||
);
|
||||
const profileResults = profileState.searchResults;
|
||||
|
||||
return (
|
||||
<div data-test="profile-search-page">
|
||||
<FormGroup>
|
||||
<Label>{profileTranslate.title}</Label>
|
||||
<Input
|
||||
name="AccountNumber"
|
||||
bsSize="sm"
|
||||
value={searchQuery}
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.target.value);
|
||||
}}
|
||||
type="text"
|
||||
placeholder={profileTranslate.placeholder}
|
||||
/>
|
||||
</FormGroup>
|
||||
{profileResults.state === AsyncState.Success ? (
|
||||
<ProfileResultsTable profiles={profileResults?.data || []} />
|
||||
) : (
|
||||
<h4>{searchQuery ? profileTranslate.loading : ""}</h4>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSearchPage;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { useParams } from "react-router-dom";
|
||||
import ProfilePage from "./ProfilePage";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {
|
||||
IEmployee,
|
||||
IEmployeeDetailsUpdate,
|
||||
IEmployeeDetailsUpdateResult
|
||||
} from "../../shared/models/Employee";
|
||||
import ProfileDao from "../../shared/dao/ProfileDao";
|
||||
import { Alert } from "reactstrap";
|
||||
|
||||
interface IRouteMatchParams {
|
||||
employeeId?: string;
|
||||
}
|
||||
|
||||
const profileEditTranslate = Translate.Get.components.profileEdit;
|
||||
|
||||
const ProfileSelector: React.FC = () => {
|
||||
const [profile, setProfile] = useState<IAsyncStateContainer<IEmployee>>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
const [updateFeedback, setUpdateFeedback] = useState<string>("");
|
||||
const [submitStatus, setUpdatedSubmitStatus] = useState<
|
||||
IAsyncStateContainer<string>
|
||||
>({
|
||||
state: AsyncState.Inactive
|
||||
});
|
||||
|
||||
const { employeeId } = useParams<IRouteMatchParams>();
|
||||
|
||||
const refreshUserProfile = () => {
|
||||
ProfileDao.getProfile(employeeId).then(
|
||||
(result) => {
|
||||
setProfile(result);
|
||||
},
|
||||
(errorResult) => {
|
||||
setProfile(errorResult);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const submitCurrentEmployeeDetailsUpdate = async (
|
||||
update: IEmployeeDetailsUpdate
|
||||
) => {
|
||||
setUpdateFeedback(profileEditTranslate.loadingDetailUpdate);
|
||||
submitStatus.state = AsyncState.Loading;
|
||||
if (update) {
|
||||
ProfileDao.submitEmployeeDetailsUpdate(update).then((result) => {
|
||||
if (result.data) {
|
||||
setSubmitStatus(result.data);
|
||||
setUpdateFeedback(result.data.updateMessage);
|
||||
refreshUserProfile();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setSubmitStatus = async (update: IEmployeeDetailsUpdateResult) => {
|
||||
if (update.updatedSuccessfully) {
|
||||
submitStatus.state = AsyncState.Success;
|
||||
} else {
|
||||
submitStatus.state = AsyncState.Failure;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshUserProfile();
|
||||
}, []);
|
||||
|
||||
if (profile.state === AsyncState.Success && profile.data)
|
||||
return (
|
||||
<ProfilePage
|
||||
profile={profile.data}
|
||||
employeeDetailsUpdateResult={updateFeedback}
|
||||
submitStatus={submitStatus}
|
||||
refreshProfile={() => refreshUserProfile()}
|
||||
submitEmployeeDetailsUpdate={(
|
||||
employeeDetailsUpdate: IEmployeeDetailsUpdate
|
||||
) => submitCurrentEmployeeDetailsUpdate(employeeDetailsUpdate)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (profile.state === AsyncState.Failure && profile.data)
|
||||
return (
|
||||
<ProfilePage
|
||||
profile={profile.data}
|
||||
employeeDetailsUpdateResult={updateFeedback}
|
||||
submitStatus={submitStatus}
|
||||
refreshProfile={() => refreshUserProfile()}
|
||||
submitEmployeeDetailsUpdate={(
|
||||
employeeDetailsUpdate: IEmployeeDetailsUpdate
|
||||
) => submitCurrentEmployeeDetailsUpdate(employeeDetailsUpdate)}
|
||||
/>
|
||||
);
|
||||
|
||||
return <h4>{profileEditTranslate.loading}</h4>;
|
||||
};
|
||||
|
||||
export default ProfileSelector;
|
||||
@@ -0,0 +1,17 @@
|
||||
## ProfileSearchPage
|
||||
|
||||
This component is used to search for a profile.
|
||||
|
||||
## ProfileResultsTable
|
||||
|
||||
This component is used to display employee search results.
|
||||
|
||||
## ProfileSelector
|
||||
|
||||
Responsible for choosing between the current appUser's
|
||||
profile and the optional route parameter if they were
|
||||
using the search or a deep link, then show that profile.
|
||||
|
||||
## ProfilePage
|
||||
|
||||
The actual profile management page.
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from "react";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import Translate from "../../translate/Translate";
|
||||
import { Button, Card, CardDeck } from "reactstrap";
|
||||
|
||||
const ReportPage: React.FC = () => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const reports = settings?.authorizedReports;
|
||||
|
||||
return reports ? (
|
||||
<div>
|
||||
<h2>{Translate.Get.containers.reportPage.title}</h2>
|
||||
<CardDeck className={"FlexCenter"}>
|
||||
<div className={"FittedCard"}>
|
||||
{reports.map((reportSettings) => {
|
||||
return (
|
||||
<Card body outline color="info">
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
window.open(reportSettings.url);
|
||||
}}
|
||||
>
|
||||
{reportSettings.name}
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardDeck>
|
||||
</div>
|
||||
) : (
|
||||
<h3>No Authorized Reports Found</h3>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportPage;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import RoleTable from "./RoleTable";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
|
||||
type RoleManagementProps = RouteComponentProps;
|
||||
|
||||
const RoleManagement: React.FC<RoleManagementProps> = () => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
return (
|
||||
<div>
|
||||
<h3>{Translate.Get.containers.roleManagement.pageTitle}</h3>
|
||||
<RoleTable
|
||||
permissionSet={settings?.permissions ?? []}
|
||||
appRoles={settings?.appRoles ?? []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleManagement;
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from "react";
|
||||
import { AutoTable } from "../../components/auto-table/AutoTable";
|
||||
import Translate from "./../../translate/Translate";
|
||||
import { IAppRole, IPermission } from "../../shared/models/Settings";
|
||||
import PermissionDisplay from "../../components/permission/PermissionDisplay";
|
||||
|
||||
export interface RoleTableProps {
|
||||
appRoles: IAppRole[];
|
||||
permissionSet: IPermission[];
|
||||
}
|
||||
|
||||
const RoleTable: React.FC<RoleTableProps> = (props) => {
|
||||
const headers = [
|
||||
Translate.Get.containers.roleManagement.roleTable.headers.name,
|
||||
Translate.Get.containers.roleManagement.roleTable.headers.description,
|
||||
Translate.Get.containers.roleManagement.roleTable.headers.permissions
|
||||
];
|
||||
|
||||
const cells = [
|
||||
(x: IAppRole) => <b>{x.name}</b>,
|
||||
(x: IAppRole) => x.description,
|
||||
(x: IAppRole) => (
|
||||
<PermissionDisplay permissions={x.permissions}></PermissionDisplay>
|
||||
)
|
||||
];
|
||||
/**
|
||||
* Sorts by Params Count, then Sorts by Alpha
|
||||
* Param Count Desc (More Params = Higher on List)
|
||||
* Alpha Desc (A on top Z on bottom)
|
||||
*/
|
||||
const CompareByPermsThenAlpha = (a: IAppRole, b: IAppRole) => {
|
||||
if (
|
||||
a.permissions.length - b.permissions.length > 0 ||
|
||||
a.name < b.name
|
||||
) {
|
||||
return -1;
|
||||
} else if (
|
||||
a.permissions.length - b.permissions.length < 0 ||
|
||||
a.name > b.name
|
||||
) {
|
||||
return 1;
|
||||
} else return 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-2">
|
||||
<h5>
|
||||
{Translate.Get.containers.roleManagement.roleTable.title}
|
||||
</h5>
|
||||
<div className="border-radius-2 border-1">
|
||||
<AutoTable
|
||||
className="border rounded"
|
||||
cells={cells}
|
||||
headers={headers}
|
||||
elements={props.appRoles.sort((a, b) =>
|
||||
CompareByPermsThenAlpha(a, b)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleTable;
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
getAllRoleMappings,
|
||||
submitRoleMappingUpdate
|
||||
} from "../../actions/RoleMappingActions";
|
||||
import { IRoleMappingState } from "../../reducers/RoleMappingReducer";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import RoleMappingTable from "../../components/role-mapping/RoleMappingTable";
|
||||
import { IRoleMappingChange } from "../../shared/models/RoleMapping";
|
||||
|
||||
const RoleMappingPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const roleMappingState = useSelector<IAppState, IRoleMappingState>(
|
||||
(state) => state.roleMappingState
|
||||
);
|
||||
|
||||
const roleMappings = roleMappingState.roleMappings.data ?? [];
|
||||
|
||||
const submitCurrentRoleMappingUpdate = async (
|
||||
update: IRoleMappingChange
|
||||
) => {
|
||||
if (roleMappingState.roleMappings.data) {
|
||||
await dispatch(submitRoleMappingUpdate(update));
|
||||
await dispatch(getAllRoleMappings());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAllRoleMappings());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<RoleMappingTable
|
||||
roleMappings={roleMappings}
|
||||
submitRoleMappingUpdate={(
|
||||
roleMappingUpdate: IRoleMappingChange
|
||||
) => submitCurrentRoleMappingUpdate(roleMappingUpdate)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoleMappingPage;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import AsyncGuard from "./AsyncGuard";
|
||||
|
||||
describe("A component for displaying async containers", () => {
|
||||
it("Displays an async status: Loading", () => {
|
||||
const container = {
|
||||
data: [],
|
||||
state: AsyncState.Loading
|
||||
};
|
||||
let wrapper = mount(
|
||||
<AsyncGuard container={container}>
|
||||
<>!!ERROR!!</>
|
||||
</AsyncGuard>
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe("Loading...");
|
||||
});
|
||||
|
||||
it("Displays an async custom status: Error", () => {
|
||||
const container = {
|
||||
data: [],
|
||||
state: AsyncState.Failure
|
||||
};
|
||||
let wrapper = mount(
|
||||
<AsyncGuard container={container} error={<h1>OH NO</h1>}>
|
||||
<>!!ERROR!!</>
|
||||
</AsyncGuard>
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe("<h1>OH NO</h1>");
|
||||
});
|
||||
|
||||
it("Displays an async child on Success", () => {
|
||||
const container = {
|
||||
data: [],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
let wrapper = mount(
|
||||
<AsyncGuard container={container}>
|
||||
<>YES</>
|
||||
</AsyncGuard>
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe("YES");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
|
||||
export interface IAsyncProps {
|
||||
container: IAsyncStateContainer<any>;
|
||||
children: JSX.Element;
|
||||
error?: JSX.Element;
|
||||
loading?: JSX.Element;
|
||||
notFound?: JSX.Element;
|
||||
inactive?: JSX.Element;
|
||||
}
|
||||
|
||||
const AsyncGuard: React.FC<IAsyncProps> = (props) => {
|
||||
switch (props.container.state) {
|
||||
case AsyncState.Inactive:
|
||||
default:
|
||||
return props.inactive ? props.inactive : <></>;
|
||||
case AsyncState.Loading:
|
||||
return props.loading ? props.loading : <>Loading...</>;
|
||||
case AsyncState.Invalid:
|
||||
case AsyncState.Failure:
|
||||
return props.error ? props.error : <>Data failed to load</>;
|
||||
case AsyncState.NotFound:
|
||||
return props.notFound ? props.notFound : <>Data was not found</>;
|
||||
case AsyncState.Success:
|
||||
return props.container.data ? (
|
||||
props.children
|
||||
) : (
|
||||
<>No Data Returned</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AsyncGuard;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import BusinessLineDisplay from "./BusinessLineDisplay";
|
||||
import { getMockAppSettings } from "../../shared/testing/mocks/MockAppSettings";
|
||||
|
||||
describe("A component for displaying business line", () => {
|
||||
const useSelectorMock = jest.spyOn(React, "useContext");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a valid business line name", () => {
|
||||
useSelectorMock.mockReturnValue(getMockAppSettings());
|
||||
let wrapper = mount(<BusinessLineDisplay businessLineCode="OPS" />);
|
||||
|
||||
expect(wrapper.html()).toBe("Operations");
|
||||
});
|
||||
|
||||
it("Displays an invalid business line name for a code", () => {
|
||||
useSelectorMock.mockReturnValue(getMockAppSettings());
|
||||
let wrapper = mount(<BusinessLineDisplay businessLineCode="XYZ" />);
|
||||
expect(wrapper.html()).toBe("Missing");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
|
||||
interface IBusinessLineDisplayProps {
|
||||
businessLineCode: string;
|
||||
}
|
||||
|
||||
const BusinessLineDisplay: React.FC<IBusinessLineDisplayProps> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const businessName = settings?.businessLines.find(
|
||||
(x) => x.code === props.businessLineCode
|
||||
);
|
||||
|
||||
return props.businessLineCode && businessName ? (
|
||||
<>{businessName.name}</>
|
||||
) : (
|
||||
<>Missing</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessLineDisplay;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import DepartmentDisplay from "./DepartmentDisplay";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { IDepartmentState } from "../../reducers/DepartmentReducer";
|
||||
import { getMockAppSettings } from "../../shared/testing/mocks/MockAppSettings";
|
||||
import { getMockDepartments } from "../../shared/testing/mocks/MockDepartments";
|
||||
|
||||
describe("A component for displaying departments", () => {
|
||||
const useContextMock = jest.spyOn(React, "useContext");
|
||||
|
||||
beforeEach(() => {
|
||||
useContextMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a valid department", () => {
|
||||
const departments: IDepartmentState = {
|
||||
departments: {
|
||||
data: [
|
||||
{
|
||||
id: 4,
|
||||
departmentId: 5,
|
||||
departmentName: "bad test",
|
||||
state: "Active",
|
||||
businessLineCode: "XY"
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
departmentId: 6,
|
||||
departmentName: "good test",
|
||||
state: "Active",
|
||||
businessLineCode: "XY"
|
||||
}
|
||||
],
|
||||
state: AsyncState.Success
|
||||
},
|
||||
submitState: {
|
||||
state: AsyncState.Success
|
||||
}
|
||||
};
|
||||
|
||||
const settings = getMockAppSettings();
|
||||
settings.departments = getMockDepartments();
|
||||
useContextMock.mockReturnValue(settings);
|
||||
const wrapper = mount(<DepartmentDisplay departmentId={55} />);
|
||||
const found = wrapper.find('[data-test="department-display-wrapper"]');
|
||||
expect(found.length).toBe(1);
|
||||
expect(found.children().first().props().title).toBe("55");
|
||||
expect(found.children().first().text()).toBe("WAUWATOSA");
|
||||
});
|
||||
|
||||
it("Displays a invalid department", () => {
|
||||
const wrapper = mount(<DepartmentDisplay departmentId={55} />);
|
||||
const found = wrapper.find('[data-test="department-display-wrapper"]');
|
||||
expect(found.length).toBe(1);
|
||||
expect(found.children().first().text()).toBe("Missing");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { SettingsContext } from "../../shared/providers/SettingsProvider";
|
||||
import Configuration from "../../shared/configuration/Configuration";
|
||||
|
||||
export enum DepartmentDisplayTargets {
|
||||
Detail = "detail",
|
||||
Dashboard = "dashboard"
|
||||
}
|
||||
interface IDepartmentDisplayProps {
|
||||
departmentId: number;
|
||||
target?: DepartmentDisplayTargets;
|
||||
nameOverride?: string;
|
||||
}
|
||||
|
||||
const DepartmentDisplay: React.FC<IDepartmentDisplayProps> = (props) => {
|
||||
const settings = React.useContext(SettingsContext);
|
||||
const department = settings?.departments.find(
|
||||
(x) => x.id === props.departmentId
|
||||
);
|
||||
|
||||
let href = Configuration.PrefixAppUrlWithBasename(
|
||||
Configuration.Get.appUrls.department
|
||||
).replace(":id", props.departmentId.toString());
|
||||
|
||||
if (props.target && props.target === DepartmentDisplayTargets.Dashboard) {
|
||||
href =
|
||||
Configuration.PrefixAppUrlWithBasename(
|
||||
Configuration.Get.appUrls.dashboard
|
||||
) +
|
||||
"?dashboard=department&departmentId=" +
|
||||
props.departmentId;
|
||||
}
|
||||
|
||||
return (
|
||||
<span data-test="department-display-wrapper">
|
||||
{props.departmentId && department ? (
|
||||
<a href={href} title={department.id + ""}>
|
||||
{props.nameOverride ?? department.departmentName}
|
||||
</a>
|
||||
) : (
|
||||
<>Missing</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default DepartmentDisplay;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import { IEmployee } from "../../shared/models/Employee";
|
||||
|
||||
export interface IEmployeeStatusGuardProps {
|
||||
employee: IEmployee;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const EmployeeStatusGuard: React.FC<IEmployeeStatusGuardProps> = (props) => {
|
||||
const shouldRenderChildren = () => {
|
||||
return props.employee && !props.employee.terminated;
|
||||
};
|
||||
|
||||
return shouldRenderChildren() ? props.children : <></>;
|
||||
};
|
||||
|
||||
export default EmployeeStatusGuard;
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import { mount } from "enzyme";
|
||||
import React from "react";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import * as reactRedux from "react-redux";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import PermissionGuard from "./PermissionGuard";
|
||||
import moment from "moment";
|
||||
import { UserType } from "../../shared/models/Auditable";
|
||||
import PermissionCode from "../../shared/enumerations/PermissionCode";
|
||||
|
||||
describe("A component for requiring permissions", () => {
|
||||
const useSelectorMock = jest.spyOn(reactRedux, "useSelector");
|
||||
const useDispatchMock = jest.spyOn(reactRedux, "useDispatch");
|
||||
|
||||
beforeEach(() => {
|
||||
useSelectorMock.mockClear();
|
||||
useDispatchMock.mockClear();
|
||||
});
|
||||
|
||||
it("Displays children when role permission present", () => {
|
||||
jest.mock("react-router-dom", () => ({
|
||||
...jest.requireActual("react-router-dom"),
|
||||
useParams: () => ({
|
||||
employeeId: "11771"
|
||||
})
|
||||
}));
|
||||
|
||||
const profile: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: "VIEW_DASHBOARD",
|
||||
description: "View Dashboard"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [
|
||||
{
|
||||
id: 1,
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: "VIEW_DASHBOARD",
|
||||
description: "View Dashboard"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
departmentId: 114,
|
||||
employeeId: "11771",
|
||||
isHome: true,
|
||||
note: "asdf",
|
||||
slidingPayout: 100,
|
||||
startDate: moment(),
|
||||
lastUpdated: moment(),
|
||||
createdByType: UserType.Associate,
|
||||
createdBy: "asdf",
|
||||
endDate: undefined,
|
||||
lastUpdatedByType: UserType.Associate,
|
||||
created: moment(),
|
||||
lastUpdatedBy: "asdf"
|
||||
}
|
||||
],
|
||||
assignments: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(profile);
|
||||
let wrapper = mount(
|
||||
<PermissionGuard requiredPermissions={["VIEW_DASHBOARD"]}>
|
||||
<h1>Authorized</h1>
|
||||
</PermissionGuard>
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe("<h1>Authorized</h1>");
|
||||
});
|
||||
|
||||
it("Displays children when additional permission present", () => {
|
||||
jest.mock("react-router-dom", () => ({
|
||||
...jest.requireActual("react-router-dom"),
|
||||
useParams: () => ({
|
||||
employeeId: "11771"
|
||||
})
|
||||
}));
|
||||
|
||||
const profile: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [],
|
||||
hasCommissions: false
|
||||
},
|
||||
additionalPermissions: [
|
||||
{
|
||||
id: 1,
|
||||
created: moment().utc(),
|
||||
createdByType: UserType.System,
|
||||
createdBy: "Tester1",
|
||||
permission: {
|
||||
code: "VIEW_DASHBOARD",
|
||||
description: "View Dashboard"
|
||||
}
|
||||
}
|
||||
],
|
||||
departments: [],
|
||||
assignments: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(profile);
|
||||
const wrapper = mount(
|
||||
<PermissionGuard
|
||||
requiredPermissions={[PermissionCode.VIEW_DASHBOARD]}
|
||||
>
|
||||
<h1>Authorized</h1>
|
||||
</PermissionGuard>
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe("<h1>Authorized</h1>");
|
||||
});
|
||||
|
||||
it("Displays nothing when permission not present", () => {
|
||||
jest.mock("react-router-dom", () => ({
|
||||
...jest.requireActual("react-router-dom"),
|
||||
useParams: () => ({
|
||||
employeeId: "11771"
|
||||
})
|
||||
}));
|
||||
|
||||
const profile: IProfileState = {
|
||||
profile: {
|
||||
data: {
|
||||
adEmail: "[email protected]",
|
||||
personId: 11,
|
||||
employeeId: "11771",
|
||||
homeDepartment: "HOME",
|
||||
baseRole: "QA",
|
||||
fullName: "John Tester",
|
||||
role: {
|
||||
name: "Admin",
|
||||
description: "admin",
|
||||
permissions: [
|
||||
{
|
||||
code: "VIEW_DASHBOARD",
|
||||
description: "View Dashboard"
|
||||
}
|
||||
],
|
||||
hasCommissions: false
|
||||
},
|
||||
additionalPermissions: [],
|
||||
departments: [],
|
||||
assignments: []
|
||||
},
|
||||
state: AsyncState.Success
|
||||
},
|
||||
searchResults: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
};
|
||||
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
useSelectorMock.mockReturnValue(profile);
|
||||
let wrapper = mount(
|
||||
<PermissionGuard requiredPermissions={["ALL_THE_THINGS"]}>
|
||||
<h1>Authorized</h1>
|
||||
</PermissionGuard>
|
||||
);
|
||||
|
||||
expect(wrapper.html()).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import { IProfileState } from "../../reducers/ProfileReducer";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { Alert } from "reactstrap";
|
||||
import ProfileService from "../../shared/services/ProfileService";
|
||||
|
||||
export interface IPermissionProps {
|
||||
requiredPermissions: string[];
|
||||
showWarning?: boolean;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
const PermissionGuard: React.FC<IPermissionProps> = (props) => {
|
||||
const profileState = useSelector<IAppState, IProfileState>(
|
||||
(state) => state.profileState
|
||||
);
|
||||
const profile = profileState.profile;
|
||||
|
||||
const shouldRenderChildren = () => {
|
||||
const requiredPermissions = props.requiredPermissions;
|
||||
if (!requiredPermissions || requiredPermissions.length < 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (profile.state !== AsyncState.Success || !profile.data) {
|
||||
return false;
|
||||
}
|
||||
const profileData = profile.data;
|
||||
return ProfileService.hasAnyPermission(
|
||||
profileData,
|
||||
requiredPermissions
|
||||
);
|
||||
};
|
||||
|
||||
if (shouldRenderChildren()) {
|
||||
return props.children;
|
||||
}
|
||||
return props.showWarning ? (
|
||||
<Alert color="danger">You do not have access.</Alert>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionGuard;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user