Adding
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user