This commit is contained in:
Zeeshaun
2026-03-23 11:21:52 -05:00
parent de9ddd5001
commit a17bc4b4a9
426 changed files with 60574 additions and 1 deletions
@@ -0,0 +1,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;
@@ -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;
@@ -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);
});
});
@@ -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;
@@ -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"
}
]
}
};
@@ -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;
@@ -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} />);
}
@@ -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;
@@ -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;
@@ -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)
);
});
});
});
@@ -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;
@@ -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>'
);
});
});
@@ -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;
@@ -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
});
});
});
@@ -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;
@@ -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"
}
]
}
};
@@ -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;
@@ -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
}
}
}
}
});
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;