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,48 @@
import React from "react";
import { mount } from "enzyme";
import GoalProgressControls from "./GoalProgressControls";
import moment from "moment";
describe("Component to select a month range", () => {
it("should provide the current month and year for to and from values", () => {
const stubOnChange = jest.fn();
mount(<GoalProgressControls onChange={stubOnChange} />);
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: moment().month() + 1,
fromYear: moment().year(),
toMonth: moment().month() + 1,
toYear: moment().year()
});
});
it("should provide the month and year for to and from values when input is changed", () => {
const stubOnChange = jest.fn();
const wrapper = mount(<GoalProgressControls onChange={stubOnChange} />);
stubOnChange.mockClear();
wrapper
.find("input[name='dashboardStartMonth']")
.simulate("change", { target: { value: "08/01/2021" } });
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: 8,
fromYear: 2021,
toMonth: moment().month() + 1,
toYear: moment().year()
});
wrapper
.find("input[name='dashboardEndMonth']")
.simulate("change", { target: { value: "9/30/2021" } });
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: 8,
fromYear: 2021,
toMonth: 9,
toYear: 2021
});
});
});
@@ -0,0 +1,85 @@
import React, { useEffect, useState } from "react";
import moment, { Moment } from "moment";
import { IMonthRange } from "../../shared/types/IMonthRange";
import { Button, Form, Input } from "reactstrap";
export interface IGoalProgressControlsProps {
onChange: (range: IMonthRange) => void;
hideTitle?: boolean;
}
const GoalProgressControls: React.FC<IGoalProgressControlsProps> = (
props: IGoalProgressControlsProps
) => {
const [fromDate, setFromDate] = useState<Moment>(moment().startOf("month"));
const [toDate, setToDate] = useState<Moment>(moment().endOf("month"));
useEffect(() => {
props.onChange({
fromMonth: fromDate.month() + 1,
fromYear: fromDate.year(),
toMonth: toDate.month() + 1,
toYear: toDate.year()
});
}, [fromDate, toDate]);
const previousMonth = () => {
const newFromMonth = moment(fromDate.subtract(1, "months"));
setFromDate(newFromMonth);
const newToMonth = moment(toDate.subtract(1, "months"));
setToDate(newToMonth);
};
const nextMonth = () => {
const newFromMonth = moment(fromDate.add(1, "months"));
setFromDate(newFromMonth);
const newToMonth = moment(toDate.add(1, "months"));
setToDate(newToMonth);
};
return (
<div className="d-flex justify-content-end">
<Form inline noValidate>
<Button size="sm" color="info" onClick={previousMonth}>
Previous Month
</Button>
<Input
name="dashboardStartMonth"
id="dashboardStartMonth"
type="month"
bsSize="sm"
required
className="ml-2"
value={fromDate.format("YYYY-MM")}
onChange={(event) => {
setFromDate(
moment(event.target.value).startOf("month")
);
}}
/>
<Input
name="dashboardEndMonth"
id="dashboardEndMonth"
type="month"
bsSize="sm"
required
className="ml-2"
value={toDate.format("YYYY-MM").toString()}
onChange={(event) => {
setToDate(moment(event.target.value).endOf("month"));
}}
/>
<Button
size="sm"
className="ml-2"
color="info"
onClick={nextMonth}
>
Next Month
</Button>
</Form>
</div>
);
};
export default GoalProgressControls;
@@ -0,0 +1,9 @@
.scrollable-top-parent {
width: 95vw;
overflow-x: auto;
transform: rotateX(180deg);
}
.scrollable-top-child {
transform: rotateX(180deg);
}
@@ -0,0 +1,50 @@
import { shallow } from "enzyme";
import React from "react";
import moment from "moment";
import { IGoal } from "../../shared/models/Goal";
import GoalTable from "./GoalTable";
import Product2 from "../../shared/enumerations/Product";
import { UserType } from "../../shared/models/Auditable";
describe("An associate views the goal audit table", () => {
it("Displays a message when there is no data to load", () => {
let wrapper = shallow(<GoalTable goals={undefined} />);
expect(wrapper.find('[data-test="no-data-message"]').length).toBe(1);
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
0
);
});
it("Displays a message when there is no data to load", () => {
let wrapper = shallow(<GoalTable goals={[]} />);
expect(wrapper.find('[data-test="no-data-message"]').length).toBe(1);
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
0
);
});
it("Does not display message when there is data", () => {
const goals: IGoal[] = [
{
id: 100,
lastUpdatedByType: UserType.System,
lastUpdatedBy: "Tester",
lastUpdated: moment().utc(),
createdByType: UserType.System,
createdBy: "Tester1",
created: moment().utc(),
employeeId: "11771",
departmentId: 56,
product: Product2.Vehicle,
startDate: moment().utc(),
endDate: moment().utc(),
value: 1
}
];
let wrapper = shallow(<GoalTable goals={goals} />);
expect(wrapper.find("[data-test='no-data-message']").length).toBe(0);
expect(wrapper.find("[data-test='autotable-container']").length).toBe(
1
);
});
});
@@ -0,0 +1,82 @@
import "./GoalTable.scss";
import React from "react";
import moment from "moment";
import { IGoal } from "../../shared/models/Goal";
import Translate from "../../translate/Translate";
import { AutoTable } from "../auto-table/AutoTable";
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
import UserDisplay from "../../containers/shared/UserDisplay";
import GoalService from "../../shared/services/GoalService";
interface IActivityTable {
goals?: IGoal[];
}
const GoalTableTranslate = Translate.Get.components.goal.goalTable;
const GoalTable: React.FC<IActivityTable> = (props) => {
const sortByUpdatedDate = (data: IGoal[]): IGoal[] => {
return data.sort((a, b) => moment(b.lastUpdated).diff(a.lastUpdated));
};
const headers = [
GoalTableTranslate.headers.id,
GoalTableTranslate.headers.product,
GoalTableTranslate.headers.departmentId,
"Start",
GoalTableTranslate.headers.value,
GoalTableTranslate.headers.personId,
GoalTableTranslate.headers.lastUpdated,
GoalTableTranslate.headers.lastUpdatedBy
];
const cells = [
(x: IGoal) => x.id,
(x: IGoal) => x.product,
(x: IGoal) => <DepartmentDisplay departmentId={x.departmentId} />,
(x: IGoal) => moment(x.startDate).format("ll"),
(x: IGoal) => GoalService.formatMonthlyGoal(x.value, x.product),
(x: IGoal) =>
x.employeeId ? (
<UserDisplay employeeId={x.employeeId} />
) : (
<DepartmentDisplay departmentId={x.departmentId} />
),
(x: IGoal) => moment.utc(x.lastUpdated).local().format("lll"),
(x: IGoal) => x.lastUpdatedBy
];
if (props.goals && props.goals.length > 0) {
return (
<div
className="scrollable-top-parent"
data-test="autotable-container"
>
<AutoTable
className="scrollable-top-child"
elements={sortByUpdatedDate(props.goals)}
headers={headers}
cells={cells}
/>
</div>
);
}
return (
<div className="scrollable-top-parent">
<div className="scrollable-top-child d-flex flex-row justify-content-center align-items-center bg-light">
<p data-test="no-data-message" className=" mt-2">
<i>{GoalTableTranslate.noData}</i>
</p>
</div>
<AutoTable
className="scrollable-top-child"
elements={[]}
headers={headers}
cells={[]}
/>
</div>
);
};
export default GoalTable;
@@ -0,0 +1,77 @@
import { shallow } from "enzyme";
import React from "react";
import moment from "moment";
import { IEmployeeGoals, IGoal } from "../../shared/models/Goal";
import Product2 from "../../shared/enumerations/Product";
import { UserType } from "../../shared/models/Auditable";
import GoalsDisplay from "./GoalsDisplay";
import { IEmployeeProgress } from "../../shared/models/Activity";
import { IEmployee } from "../../shared/models/Employee";
describe("Display an employee goal and progress", () => {
const mockUser: IEmployee = {
role: {
name: "baseRole",
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,
role: {
name: "role",
description: "role",
permissions: [],
hasCommissions: true
},
slidingPayout: 100
}
],
baseRole: "none",
homeDepartment: "none",
personId: 555,
adEmail: "[email protected]",
employeeId: "11771",
fullName: "Test McTester",
additionalPermissions: [],
assignments: []
};
it("Displays goal and progress table", () => {
const userGoals: IEmployeeGoals = {
month: 6,
year: 2021,
user: mockUser,
goals: {},
departments: [],
goalAudit: []
};
const userProgress: IEmployeeProgress = {
user: mockUser,
month: 6,
year: 2021,
activityAudit: [],
departments: [],
progress: {}
};
let wrapper = shallow(
<GoalsDisplay userGoals={userGoals} userProgress={userProgress} />
);
expect(
wrapper.find("[data-test='goal-progress-table-view']").length
).toBe(1);
});
});
@@ -0,0 +1,43 @@
import React from "react";
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
import { IEmployeeProgress } from "../../shared/models/Activity";
import ProductTableHeader from "../shared/ProductTableHeader";
import ProgressTableRow from "../shared/ProgressTableRow";
export interface IGoalsDisplayProps {
userProgress: IEmployeeProgress;
}
const GoalsDisplay: React.FC<IGoalsDisplayProps> = (
props: IGoalsDisplayProps
) => {
const goals = props.userProgress.goals;
const departments = props.userProgress.departments;
const progress = props.userProgress.progress;
const rows = departments.map((department) => {
const goal = goals[department.departmentId];
const activity = progress[department.departmentId];
return (
<ProgressTableRow
label={
<DepartmentDisplay departmentId={department.departmentId} />
}
progress={activity}
goals={goal}
/>
);
});
return (
<table
className="table table-striped tablecenterCSS tablecenterheadCSS"
data-test="goal-progress-table-view"
>
<ProductTableHeader />
<tbody>{rows}</tbody>
</table>
);
};
export default GoalsDisplay;
@@ -0,0 +1,121 @@
import React from "react";
import { mount } from "enzyme";
import MonthRangeControl from "./MonthRangeControl";
describe("Component to select a month range with no state", () => {
// If you are reading this test, this is what is breaking all the useEffects.
// When you have children with useEffect and useState AND a Parent, you'll double render! 2^useEffect in the tree.
// Compare this to GoalProgressControls...
it("should render", () => {
const stubOnChange = jest.fn();
const range = {
fromMonth: 7,
fromYear: 2021,
toMonth: 7,
toYear: 2021
};
mount(<MonthRangeControl onChange={stubOnChange} monthRange={range} />);
expect(stubOnChange).not.toHaveBeenCalledWith(range);
});
it("should provide the from month when input is changed", () => {
const stubOnChange = jest.fn();
const range = {
fromMonth: 7,
fromYear: 2021,
toMonth: 7,
toYear: 2021
};
const wrapper = mount(
<MonthRangeControl onChange={stubOnChange} monthRange={range} />
);
stubOnChange.mockClear();
wrapper
.find("#fromMonth")
.simulate("change", { target: { value: "8" } });
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: 8,
fromYear: 2021,
toMonth: 7,
toYear: 2021
});
});
it("should provide the to month when input is changed", () => {
const stubOnChange = jest.fn();
const range = {
fromMonth: 7,
fromYear: 2021,
toMonth: 7,
toYear: 2021
};
const wrapper = mount(
<MonthRangeControl onChange={stubOnChange} monthRange={range} />
);
stubOnChange.mockClear();
wrapper.find("#toMonth").simulate("change", { target: { value: "9" } });
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: 7,
fromYear: 2021,
toMonth: 9,
toYear: 2021
});
});
it("should provide the from year when input is changed", () => {
const stubOnChange = jest.fn();
const range = {
fromMonth: 7,
fromYear: 2021,
toMonth: 7,
toYear: 2021
};
const wrapper = mount(
<MonthRangeControl onChange={stubOnChange} monthRange={range} />
);
stubOnChange.mockClear();
wrapper
.find("#fromYear")
.simulate("change", { target: { value: "2022" } });
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: 7,
fromYear: 2022,
toMonth: 7,
toYear: 2021
});
});
it("should provide the to year when input is changed", () => {
const stubOnChange = jest.fn();
const range = {
fromMonth: 7,
fromYear: 2021,
toMonth: 7,
toYear: 2021
};
const wrapper = mount(
<MonthRangeControl onChange={stubOnChange} monthRange={range} />
);
stubOnChange.mockClear();
wrapper.find("#toYear").simulate("change", {
target: { value: "2023" }
});
expect(stubOnChange).toHaveBeenCalledWith({
fromMonth: 7,
fromYear: 2021,
toMonth: 7,
toYear: 2023
});
});
});
@@ -0,0 +1,102 @@
import React from "react";
import moment from "moment";
import GoalService from "../../shared/services/GoalService";
import { IMonthRange } from "../../shared/types/IMonthRange";
export interface IGoalProgressControlsProps {
onChange: (range: IMonthRange) => void;
monthRange: IMonthRange;
}
const MonthRangeControl: React.FC<IGoalProgressControlsProps> = ({
monthRange,
onChange
}) => {
const setMonthRange = (monthRange: IMonthRange) => {
onChange(monthRange);
};
const years = [moment().year() - 2, moment().year() - 1, moment().year()];
return (
<span>
<div className="navbar-brand">
<select
className="mx-1"
id="fromMonth"
value={monthRange.fromMonth}
onChange={(event) => {
setMonthRange({
...monthRange,
fromMonth: parseInt(event.target.value, 10)
});
}}
>
{GoalService.Months.map(
(monthStr: string, monthNum: number) => (
<option key={monthNum} value={monthNum + 1}>
{monthStr}
</option>
)
)}
</select>
<select
className="mx-1"
id="fromYear"
value={monthRange.fromYear}
onChange={(event) => {
setMonthRange({
...monthRange,
fromYear: parseInt(event.target.value, 10)
});
}}
>
{years.map((year: number) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
to
<select
className="mx-1"
id="toMonth"
value={monthRange.toMonth}
onChange={(event) => {
setMonthRange({
...monthRange,
toMonth: parseInt(event.target.value, 10)
});
}}
>
{GoalService.Months.map(
(monthStr: string, monthNum: number) => (
<option key={monthNum} value={monthNum + 1}>
{monthStr}
</option>
)
)}
</select>
<select
className="mx-1"
id="toYear"
value={monthRange.toYear}
onChange={(event) => {
setMonthRange({
...monthRange,
toYear: parseInt(event.target.value, 10)
});
}}
>
{years.map((year: number) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
</div>
</span>
);
};
export default MonthRangeControl;
@@ -0,0 +1,80 @@
import { shallow } from "enzyme";
import React from "react";
import moment from "moment";
import { IEmployeeGoals } from "../../shared/models/Goal";
import { UserType } from "../../shared/models/Auditable";
import TabbedProgressView from "./TabbedProgressView";
import { IEmployeeProgress } from "../../shared/models/Activity";
import { IEmployee } from "../../shared/models/Employee";
describe("Basic tabbed goals and activity progress", () => {
const mockUser: IEmployee = {
role: {
name: "baseRole",
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,
role: {
name: "role",
description: "role",
permissions: [],
hasCommissions: true
},
slidingPayout: 100
}
],
baseRole: "none",
homeDepartment: "none",
personId: 555,
adEmail: "[email protected]",
employeeId: "11771",
fullName: "Test McTester",
assignments: [],
additionalPermissions: []
};
it("Displays the table", () => {
const userGoals: IEmployeeGoals = {
month: 6,
year: 2021,
user: mockUser,
goals: {},
departments: [],
goalAudit: []
};
const userProgress: IEmployeeProgress = {
user: mockUser,
month: 6,
year: 2021,
activityAudit: [],
departments: [],
progress: {}
};
const wrapper = shallow(
<TabbedProgressView
userGoals={userGoals}
userProgress={userProgress}
/>
);
expect(wrapper.find("[data-test='progress-tabbed-view']").length).toBe(
1
);
});
});
@@ -0,0 +1,53 @@
import React from "react";
import Translate from "./../../translate/Translate";
import { ActivityType, IEmployeeProgress } from "../../shared/models/Activity";
import TabView from "../tab-view/TabView";
import GoalsDisplay from "./GoalsDisplay";
import GoalTable from "./GoalTable";
import ActivityTableWithStateFilter from "../activity/activity-table/ActivityTableWithStateFilter";
export interface IGoalsDisplayProps {
userProgress: IEmployeeProgress;
}
const TabViewTranslate = Translate.Get.containers.progressView;
const TabbedProgressView: React.FC<IGoalsDisplayProps> = (
props: IGoalsDisplayProps
) => {
return (
<TabView
data-test="progress-tabbed-view"
pages={[
{
header: TabViewTranslate.progressTabGroup.progress,
element: <GoalsDisplay userProgress={props.userProgress} />
},
{
header: TabViewTranslate.progressTabGroup.goalAudit,
element: <GoalTable goals={props.userProgress.goalAudit} />
},
{
header: TabViewTranslate.progressTabGroup.activitySales,
element: (
<ActivityTableWithStateFilter
activities={props.userProgress.activityAudit}
activityTypeFilter={ActivityType.Sale}
/>
)
},
{
header: TabViewTranslate.progressTabGroup.activityReferrals,
element: (
<ActivityTableWithStateFilter
activities={props.userProgress.activityAudit}
activityTypeFilter={ActivityType.Referral}
/>
)
}
]}
/>
);
};
export default TabbedProgressView;
@@ -0,0 +1,65 @@
import { mount, shallow, ShallowWrapper } from "enzyme";
import React from "react";
import AsyncState from "../../../shared/enumerations/AsyncState";
import Translate from "../../../translate/Translate";
import DispersalFormAlertBar from "./DispersalFormAlertBar";
let wrapper: any;
const DFATranslate =
Translate.Get.components.goal.goalDispersal.dispersalFormAlertBar;
describe("Alert bar displays expected message", () => {
it("Displays loading messsage when loading", () => {
wrapper = mount(
<DispersalFormAlertBar
goalDispersalReceiptContainer={{ state: AsyncState.Loading }}
/>
);
expect(
wrapper.find(`[data-test="loading"]`).hostNodes().text()
).toContain(DFATranslate.loading);
});
it("Displays loading messsage when error", () => {
wrapper = mount(
<DispersalFormAlertBar
goalDispersalReceiptContainer={{ state: AsyncState.Failure }}
/>
);
expect(
wrapper.find(`[data-test="error"]`).hostNodes().text()
).toContain(DFATranslate.failure);
});
it("Displays success messsage when success", () => {
wrapper = mount(
<DispersalFormAlertBar
goalDispersalReceiptContainer={{
data: { departmentId: 10, goalDispersalErrors: [] },
state: AsyncState.Success
}}
/>
);
expect(
wrapper.find(`[data-test="success"]`).hostNodes().text()
).toContain(DFATranslate.success.alerts);
expect(
wrapper.find(`[data-test="success"]`).hostNodes().text()
).toContain(DFATranslate.success.message);
});
it("Displays sucess messsage when success and no data", () => {
wrapper = mount(
<DispersalFormAlertBar
goalDispersalReceiptContainer={{ state: AsyncState.Success }}
/>
);
expect(
wrapper.find(`[data-test="success"]`).hostNodes().text()
).not.toContain(DFATranslate.success.alerts);
expect(
wrapper.find(`[data-test="success"]`).hostNodes().text()
).toContain(DFATranslate.success.message);
});
});
@@ -0,0 +1,150 @@
import React from "react";
import { shallow, ShallowWrapper } from "enzyme";
import DispersalForm from "./DispersalForm";
import AsyncState from "./../../../shared/enumerations/AsyncState";
import Translate from "../../../translate/Translate";
import { AllowedGoalDispersalStrategy } from "../../../shared/models/GoalDispersal";
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<DispersalForm
confirmDispersal={jest.fn()}
submitState={AsyncState.Inactive}
isOpen
/>
);
});
const DFTranslate = Translate.Get.components.goal.goalDispersal.dispersalForm;
describe("User Opens Dispersal Form", () => {
it("Displays placeholder data for drop down and description", () => {
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
expect(
wrapper.find('[data-test="strategy-description"]').text()
).toContain(DFTranslate.description.placeholder);
});
it("disables submit", () => {
expect(wrapper.find("Button").prop("disabled")).toBe(true);
});
});
describe("User Closes Dispersal Form", () => {
it("displays the proper description based on selection", () => {
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals.toString()
}
});
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe(
AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals.toString()
);
expect(wrapper.find('[data-test="strategy-description"]').text()).toBe(
DFTranslate.description.extrapolateFromExisting
);
});
it("disables submit until an option is selected", () => {
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals.toString()
}
});
expect(wrapper.find('[data-test="strategy-description"]').text()).toBe(
DFTranslate.description.extrapolateFromExisting
);
expect(wrapper.find("Button").prop("disabled")).toBe(false);
});
it("does not allow submit when loading", () => {
wrapper.setProps({ submitState: AsyncState.Loading });
wrapper.update();
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals.toString()
}
});
expect(wrapper.find('[data-test="strategy-description"]').text()).toBe(
DFTranslate.description.extrapolateFromExisting
);
expect(wrapper.find("Button").prop("disabled")).toBe(true);
});
});
describe("User Opens Dispersal From and Selects CustomWeightedDispersal", () => {
it("Displays the correct descripton for CustomWeightedDispersal", () => {
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.CustomWeightedDispersal.toString()
}
});
expect(wrapper.find('[data-test="strategy-description"]').text()).toBe(
DFTranslate.description.customWeighted
);
});
it("Hides Default Submit Button and Warning", () => {
expect(wrapper.find("Button").prop("hidden")).toBe(false);
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.CustomWeightedDispersal.toString()
}
});
expect(wrapper.find('[data-test="strategy-description"]').text()).toBe(
DFTranslate.description.customWeighted
);
expect(wrapper.find("Button").prop("hidden")).toBe(true);
});
it("Shows the CustomWeightedDispersalForm When Strategy is Selected", () => {
expect(
wrapper
.find('[data-test="custom-weighted-dispersal-dropdown"]')
.prop("isOpen")
).toBe(false);
expect(
wrapper.find('[data-test="select-strategy"]').prop("value")
).toBe("");
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals.toString()
}
});
expect(
wrapper
.find('[data-test="custom-weighted-dispersal-dropdown"]')
.prop("isOpen")
).toBe(false);
wrapper.find('[data-test="select-strategy"]').invoke("onChange")({
target: {
value: AllowedGoalDispersalStrategy.CustomWeightedDispersal.toString()
}
});
expect(wrapper.find('[data-test="strategy-description"]').text()).toBe(
DFTranslate.description.customWeighted
);
expect(
wrapper
.find('[data-test="custom-weighted-dispersal-dropdown"]')
.prop("isOpen")
).toBe(true);
});
});
@@ -0,0 +1,164 @@
import React from "react";
import {
Button,
Collapse,
Dropdown,
FormGroup,
Input,
Label
} from "reactstrap";
import { useState } from "react";
import { useEffect } from "react";
import { AllowedGoalDispersalStrategy } from "../../../shared/models/GoalDispersal";
import AsyncState from "../../../shared/enumerations/AsyncState";
import Translate from "./../../../translate/Translate";
import CustomWeightedDispersalForm from "./custom-weighted-dispersal/CustomWeightedDispersalForm";
import { IProductDetail } from "../../../shared/enumerations/Product";
import { SettingsContext } from "../../../shared/providers/SettingsProvider";
import { ICustomWeightedDispersalFormData } from "./../../../shared/models/GoalDispersal";
export interface IDispersalForm {
isOpen?: boolean;
confirmStatelessDispersal: (
selectedStrategy: AllowedGoalDispersalStrategy
) => void;
confirmWeightedDispersal: (
formData: ICustomWeightedDispersalFormData
) => void;
submitState: AsyncState;
}
const DFTranslate = Translate.Get.components.goal.goalDispersal.dispersalForm;
const DispersalForm: React.FC<IDispersalForm> = (props) => {
const [selected, selectOption] = useState<string>("");
const settings = React.useContext(SettingsContext);
useEffect(() => {
selectOption("");
}, [props.isOpen]);
useEffect(() => {
if (props.submitState === AsyncState.Success) {
selectOption("");
}
}, [props.submitState]);
const getDescription = () => {
switch (selected) {
case AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals.toString():
return DFTranslate.description.extrapolateFromExisting;
case AllowedGoalDispersalStrategy.CustomWeightedDispersal.toString():
return DFTranslate.description.customWeighted;
default:
return DFTranslate.description.placeholder;
}
};
const doesStrategyHaveForm = () => {
if (selected === AllowedGoalDispersalStrategy.CustomWeightedDispersal) {
return true;
} else return false;
};
const getStrategyFromSelected = (): AllowedGoalDispersalStrategy => {
const key = selected as keyof typeof AllowedGoalDispersalStrategy;
return AllowedGoalDispersalStrategy[key];
};
return (
<Collapse isOpen={props.isOpen}>
<hr />
<div className="d-flex flex-row justify-content-center">
<FormGroup>
<Label>{DFTranslate.dispersalStrategyLabel}</Label>
<Input
data-test="select-strategy"
type="select"
value={selected}
onChange={(event) => {
selectOption(event.target.value.toString());
}}
>
<option
data-test="strategy=placeholder"
hidden={selected !== ""}
value={""}
>
{DFTranslate.dispersalStrategyPlaceholder}
</option>
{Object.values(AllowedGoalDispersalStrategy)
.filter((key) => isNaN(Number(key)))
.map((value) => {
return (
<option key={value} value={value}>
{value}
</option>
);
})}
</Input>
</FormGroup>
<FormGroup
className="ml-3"
style={{ maxWidth: "25em", minWidth: "25em" }}
>
<Label className="mb-0">
{DFTranslate.dispersalStrategyDescLabel}
</Label>
<p
data-test="strategy-description"
className="p-0 m-0 text-muted"
>
{getDescription()}
</p>
</FormGroup>
<FormGroup className="my-auto d-flex flex-column">
<Button
hidden={doesStrategyHaveForm()}
onClick={() => {
props.confirmStatelessDispersal(
getStrategyFromSelected()
);
}}
disabled={
doesStrategyHaveForm() ||
selected === "" ||
props.submitState === AsyncState.Loading
}
color="primary"
>
{DFTranslate.confirmButton}
</Button>
<small
hidden={doesStrategyHaveForm()}
className="text-sm text-danger mb-0"
>
{DFTranslate.warningText}
</small>
</FormGroup>
{/* Are there additonally required fields? Display them here. Hide this submit button and provide your own in your details component*/}
</div>
<Collapse
data-test="custom-weighted-dispersal-dropdown"
isOpen={
selected ===
AllowedGoalDispersalStrategy.CustomWeightedDispersal
}
>
<hr />
<div className="px-4 pb-2">
<CustomWeightedDispersalForm
selectedStrategy={getStrategyFromSelected()}
confirmDispersal={(formData) => {
props.confirmWeightedDispersal(formData);
}}
products={settings?.productDetails ?? []}
roles={settings?.appRoles ?? []}
/>
</div>
</Collapse>
</Collapse>
);
};
export default DispersalForm;
@@ -0,0 +1,102 @@
import React from "react";
import { Alert, Spinner } from "reactstrap";
import AsyncState from "../../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import { IGoalDispersalReceipt } from "../../../shared/models/GoalDispersal";
import DispersalReceiptErrorDisplay from "./DispersalReceiptErrorDisplay";
import Translate from "./../../../translate/Translate";
export interface IDispersalFormAlertBarProps {
goalDispersalReceiptContainer: IAsyncStateContainer<IGoalDispersalReceipt>;
}
const DFATranslate =
Translate.Get.components.goal.goalDispersal.dispersalFormAlertBar;
const DispersalFormAlertBar: React.FC<IDispersalFormAlertBarProps> = (
props
) => {
if (props.goalDispersalReceiptContainer.state === AsyncState.Loading) {
return (
<Alert
className="py-1 mx-3 my-1"
color="warning"
data-test="loading"
>
<div className="d-flex flex-col justify-content-start align-items-center">
<div className="mr-4">
<Spinner className="m-0" color="warning" />
</div>
<div>
<p className="mb-0">{DFATranslate.loading}</p>
</div>
</div>
</Alert>
);
} else if (
props.goalDispersalReceiptContainer.state === AsyncState.Failure
) {
return (
<Alert className="py-1 mx-3 my-1" color="danger" data-test="error">
<div className="d-flex flex-col justify-content-start align-items-center">
<div className="mr-4">
<span className="fa fa-times-circle m-0" />
</div>
<div>
<p className="mb-0">{DFATranslate.failure}</p>
</div>
</div>
</Alert>
);
} else if (
props.goalDispersalReceiptContainer.state == AsyncState.Success &&
props.goalDispersalReceiptContainer.data
) {
return (
<Alert
className="py-1 mx-3 my-1"
color="success"
data-test="success"
>
<div className="d-flex flex-col justify-content-start align-items-center">
<div className="mr-4">
<span className="fa fa-check-circle m-0" />
</div>
<div>
<p className="mb-0">{DFATranslate.success.message}</p>
<hr />
<b>{DFATranslate.success.alerts}</b>
<DispersalReceiptErrorDisplay
dispersalReceipt={
props.goalDispersalReceiptContainer.data
}
/>
</div>
</div>
</Alert>
);
} else if (
props.goalDispersalReceiptContainer.state == AsyncState.Success
) {
return (
<Alert
className="py-1 mx-3 my-1"
color="success"
data-test="success"
>
<div className="d-flex flex-col justify-content-start align-items-center">
<div className="mr-4">
<span className="fa fa-check-circle m-0" />
</div>
<div>
<p className="mb-0">{DFATranslate.success.message}</p>
</div>
</div>
</Alert>
);
}
return <div />;
};
export default DispersalFormAlertBar;
@@ -0,0 +1,122 @@
import React from "react";
import { shallow, ShallowWrapper } from "enzyme";
import DispersalReceiptErrorDisplay from "./DispersalReceiptErrorDisplay";
import Translate from "../../../translate/Translate";
const DEDTranslate =
Translate.Get.components.goal.goalDispersal.dispersalReceiptErrorDisplay;
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<DispersalReceiptErrorDisplay
dispersalReceipt={{
departmentId: 10,
goalDispersalErrors: [
{
errorType: "DispersedToUserWithoutPermission",
message: "this doesn't display",
employeeId: "emp1",
employeeName: "John Wick"
},
{
errorType: "NewUserRequiresManualEntry",
message: "this doesn't display",
employeeId: "emp2",
employeeName: "Batman"
},
{
errorType: "UserNoLongerInDepartment",
message: "this doesn't display",
employeeId: "emp3",
employeeName: "Superman"
},
{
errorType: "ERROR",
message: "this doesn't display",
employeeId: "emp4",
employeeName: "Megaman"
},
{
errorType: "DispersedToUserWithoutPermission",
message: "this doesn't display",
employeeId: "emp5"
}
]
}}
/>
);
});
describe("DispersalFormReceiptErrorDisplay has to display every error type", () => {
it("Properly displays new appUser error", () => {
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp2")
.text()
).toContain(DEDTranslate.userRequiresManualEntry);
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp2")
.text()
).toContain("Batman");
});
it("Properly displays permission error", () => {
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp1")
.text()
).toContain(DEDTranslate.userWithoutPermission);
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp1")
.text()
).toContain("John Wick");
});
it("Properly displays appUser no longer ind epartment error", () => {
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp3")
.text()
).toContain(DEDTranslate.userNoLongerInDepartment);
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp3")
.text()
).toContain("Superman");
});
it("Properly displays 'other' error", () => {
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp4")
.text()
).toContain(DEDTranslate.default);
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp4")
.text()
).toContain("Megaman");
});
it("Falls back to employeeID when no name is available", () => {
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp5")
.text()
).toContain(DEDTranslate.userWithoutPermission);
expect(
wrapper
.find('[data-test="error-row"]')
.findWhere((x) => x.key() == "emp5")
.text()
).toContain("emp5");
});
});
@@ -0,0 +1,42 @@
import React from "react";
import { IGoalDispersalReceipt } from "../../../shared/models/GoalDispersal";
import Translate from "./../../../translate/Translate";
export interface IDispersalReceiptErrorDisplayProps {
dispersalReceipt: IGoalDispersalReceipt;
}
const DEDTranslate =
Translate.Get.components.goal.goalDispersal.dispersalReceiptErrorDisplay;
const GetMesageFromErrorType = (errorType: string) => {
switch (errorType) {
case "DispersedToUserWithoutPermission":
return DEDTranslate.userWithoutPermission;
case "NewUserRequiresManualEntry":
return DEDTranslate.userRequiresManualEntry;
case "UserNoLongerInDepartment":
return DEDTranslate.userNoLongerInDepartment;
default:
return DEDTranslate.default;
}
};
const DispersalReceiptErrorDisplay: React.FC<IDispersalReceiptErrorDisplayProps> = (
props
) => {
return (
<div data-test="error-row">
{props.dispersalReceipt.goalDispersalErrors.map((error) => {
return (
<p className="mb-0" key={error.employeeId}>
{error.employeeName ?? error.employeeId} {"->"}{" "}
{GetMesageFromErrorType(error.errorType)}
</p>
);
})}
</div>
);
};
export default DispersalReceiptErrorDisplay;
@@ -0,0 +1,307 @@
import React from "react";
import { ShallowWrapper, shallow } from "enzyme";
import { IProductDetail } from "../../../../shared/enumerations/Product";
import { AllowedGoalDispersalStrategy } from "../../../../shared/models/GoalDispersal";
import { IAppRole } from "../../../../shared/models/Settings";
import CustomWeightedDispersalForm from "./CustomWeightedDispersalForm";
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<CustomWeightedDispersalForm
selectedStrategy={
AllowedGoalDispersalStrategy.ExtrapolateFromExistingGoals
}
products={productDetails}
roles={roles}
confirmDispersal={jest.fn()}
/>
);
});
describe("Submit Button is Disabled until percentage add up to 100.", () => {
beforeEach(() => {
wrapper.find('[name="DispersalProduct"]').invoke("onChange")({
target: { value: "ATMCard" }
});
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "Admin",
value: 50
}
]);
});
it("Allows Submit when all inputs are populated", () => {
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(true);
});
it("Disables submit if any given percentage is above 100%", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "LIC",
value: 100
}
]);
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(false);
});
it("Disables submit if any given percentage is above 100%", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "LIC",
value: 101
}
]);
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(true);
});
});
describe("Submit Button is Disabled until FormState is valid", () => {
beforeEach(() => {
wrapper.find('[name="DispersalProduct"]').invoke("onChange")({
target: { value: "ATMCard" }
});
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "Admin",
value: 100
}
]);
});
it("Allows Submit when all inputs are populated", () => {
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(false);
});
it("Disables submit when product is not selected", () => {
wrapper.find('[name="DispersalProduct"]').invoke("onChange")({
target: { value: "" }
});
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(true);
});
it("Disables submit when there are placeholder Roles in form state", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "PLACE-HOLDER-ROW",
value: 50
}
]);
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(true);
});
it("Disables submit if any given percentage is below 0%", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "LIC",
value: -1
}
]);
expect(
wrapper.find('[name="SubmitDispersalButton"]').prop("disabled")
).toBe(true);
});
});
describe("A User Opens the Form", () => {
it("Starts with an expected placeholder role value", () => {
wrapper.find('[name="DispersalProduct"]').invoke("onChange")({
target: { value: "ATMCard" }
});
expect(
wrapper
.find('[data-test="dispersal-weight-input"]')
.prop("weightsByRole")
).toEqual([{ roleName: "PLACE-HOLDER-ROW", value: 0 }]);
});
});
describe("A User adds a new role", () => {
beforeEach(() => {
wrapper.find('[name="DispersalProduct"]').invoke("onChange")({
target: { value: "ATMCard" }
});
});
it("Starts with add role disabled", () => {
expect(wrapper.find('[name="AddNewRoleButton"]').prop("disabled")).toBe(
true
);
expect(
wrapper
.find('[data-test="dispersal-weight-input"]')
.prop("weightsByRole")
).toEqual([{ roleName: "PLACE-HOLDER-ROW", value: 0 }]);
});
it("Enables the add button when a role is picked", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "Admin",
value: 50
}
]);
expect(wrapper.find('[name="AddNewRoleButton"]').prop("disabled")).toBe(
false
);
});
it("Creates new role with a placeholder value when clicked", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "Admin",
value: 50
}
]);
expect(
wrapper
.find('[data-test="dispersal-weight-input"]')
.prop("weightsByRole")
).toEqual([{ roleName: "Admin", value: 50 }]);
wrapper.find('[name="AddNewRoleButton"]').invoke("onClick")(undefined);
expect(
wrapper
.find('[data-test="dispersal-weight-input"]')
.prop("weightsByRole")
).toEqual([
{ roleName: "Admin", value: 50 },
{ roleName: "PLACE-HOLDER-ROW", value: 0 }
]);
});
it("Does not allow a new role to be added until all roles populated", () => {
wrapper
.find('[data-test="dispersal-weight-input"]')
.invoke("handleChange")([
{
roleName: "Admin",
value: 50
}
]);
wrapper.find('[name="AddNewRoleButton"]').invoke("onClick")(undefined);
expect(
wrapper
.find('[data-test="dispersal-weight-input"]')
.prop("weightsByRole")
).toEqual([
{ roleName: "Admin", value: 50 },
{ roleName: "PLACE-HOLDER-ROW", value: 0 }
]);
expect(wrapper.find('[name="AddNewRoleButton"]').prop("disabled")).toBe(
true
);
});
});
const roles: IAppRole[] = [
{
name: "Admin",
description: "Admin",
permissions: [],
hasCommissions: false,
rank: 99
},
{
name: "LIC",
description: "Landmark Investment Center",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "RegionalManager",
description: "Regional Manager",
permissions: [],
hasCommissions: true,
rank: 97
},
{
name: "BM",
description: "Branch Manager",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "ABM",
description: "Assistant Branch Manager",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "BOM",
description: "Branch Operations Manager",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "PFO",
description: "PERS FIN OFFICER",
permissions: [],
hasCommissions: true,
rank: 1
}
];
const productDetails = ([
{
product: "ATMCard",
title: "ATM Card",
isSellable: false,
isReferrable: true,
isNonPaying: true
},
{
product: "CreditCardBalanceTransfer",
title: "CCBT",
isSellable: false,
isReferrable: true,
isNonPaying: false
},
{
product: "NewChecking",
title: "New Checking",
isSellable: true,
isReferrable: true,
isNonPaying: false
},
{
product: "GAPInsurance",
title: "Gap Insurance",
isSellable: true,
isReferrable: false,
isNonPaying: false
},
{
product: "CreditCard",
title: "Credit Card",
isSellable: false,
isReferrable: true,
isNonPaying: false
}
] as unknown) as IProductDetail[];
@@ -0,0 +1,188 @@
import React, { useState, useEffect } from "react";
import { IProductDetail } from "../../../../shared/enumerations/Product";
import {
AllowedGoalDispersalStrategy,
IRoleDispersalWeight,
ICustomWeightedDispersalRequest,
ICustomWeightedDispersalFormData
} from "../../../../shared/models/GoalDispersal";
import { Button, Col, FormFeedback, FormGroup, Input, Row } from "reactstrap";
import DispersalWeightInput from "./DispersalWeightInput";
import { IAppRole } from "../../../../shared/models/Settings";
import _ from "lodash";
import Translate from "../../../../translate/Translate";
export interface ICustomWeightedDispersalForm {
selectedStrategy: AllowedGoalDispersalStrategy;
products: IProductDetail[];
roles: IAppRole[];
confirmDispersal: (formData: ICustomWeightedDispersalFormData) => void;
}
const PlaceHolderRoleName = "PLACE-HOLDER-ROW";
const getDefaultFormState = (): ICustomWeightedDispersalFormData => {
return {
dispersalProduct: "",
weightsByRole: [{ roleName: PlaceHolderRoleName, value: 0 }]
};
};
const _translate =
Translate.Get.components.goal.goalDispersal.customWeightedDispersal
.customWeightedDispersalForm;
const CustomWeightedDispersalForm: React.FC<ICustomWeightedDispersalForm> = (
props
) => {
useEffect(() => {
setFormState(getDefaultFormState());
}, [props.selectedStrategy]);
const [
formState,
setFormState
] = useState<ICustomWeightedDispersalFormData>(getDefaultFormState());
const hasPlaceholderRow = (): boolean => {
return formState.weightsByRole.some(
(x) => x.roleName === PlaceHolderRoleName
);
};
const weightsAddUpTo100 = (): boolean => {
return formState.weightsByRole.reduce((a, b) => a + b.value, 0) === 100;
};
const addNewPlaceholderRow = () => {
if (hasPlaceholderRow()) {
return;
}
setFormState({
...formState,
weightsByRole: [
...formState.weightsByRole,
{ roleName: PlaceHolderRoleName, value: 0 }
]
});
};
const handleWeightUpdate = (newWeights: IRoleDispersalWeight[]) => {
setFormState({
...formState,
weightsByRole: newWeights
});
};
const isFormStateValid = () => {
const noPlaceHolderRows = !hasPlaceholderRow();
const weightsAreBetween0and100 = formState.weightsByRole.every(
(x) => x.value >= 0 && x.value <= 100
);
const dispersalProductSelected = formState.dispersalProduct != "";
return (
noPlaceHolderRows &&
weightsAreBetween0and100 &&
dispersalProductSelected &&
weightsAddUpTo100()
);
};
const products = props.products.filter((x) => x.hasGoals);
return (
<form>
<Row className="mb-2" form>
<Col>
<FormGroup>
<label>
{_translate.productTypeInput.label}
<span className="text-danger">*</span>
</label>
<Input
name="DispersalProduct"
type="select"
bsSize="sm"
required
invalid={!formState.dispersalProduct}
value={formState.dispersalProduct}
onChange={(event) => {
setFormState({
...formState,
dispersalProduct: event.target.value
});
}}
>
<option key={undefined} value="">
{_translate.productTypeInput.placeholder}
</option>
{products.map((product) => {
return (
<option
key={product.product}
value={product.product}
>
{product.title}
</option>
);
})}
</Input>
<FormFeedback>
{_translate.productTypeInput.validation}
</FormFeedback>
</FormGroup>
</Col>
<Col>
<DispersalWeightInput
data-test="dispersal-weight-input"
weightsByRole={formState.weightsByRole}
roles={props.roles}
handleChange={(newWeights) => {
handleWeightUpdate(newWeights);
}}
/>
<Button
name="AddNewRoleButton"
size="sm"
disabled={hasPlaceholderRow() == true}
onClick={() => addNewPlaceholderRow()}
className="align-self-end"
>
{_translate.addRoleButton}
</Button>
</Col>
</Row>
<Row className="d-flex flex-row justify-content-end mr-2">
<div>
{weightsAddUpTo100() ? (
""
) : (
<small className="text-sm text-danger mb-0">
{_translate.addTo100Total}
</small>
)}
</div>
</Row>
<Row className="d-flex flex-row justify-content-end mr-2">
<div>
<Button
name="SubmitDispersalButton"
color="primary"
disabled={!isFormStateValid()}
onClick={() => props.confirmDispersal(formState)}
>
{_translate.submitButton}
</Button>
<br></br>
<small className="text-sm text-danger mb-0">
{_translate.submitButtonWarning}
</small>
</div>
</Row>
</form>
);
};
export default CustomWeightedDispersalForm;
@@ -0,0 +1,228 @@
import React from "react";
import { ShallowWrapper, shallow } from "enzyme";
import DispersalWeightInput from "./DispersalWeightInput";
import { IAppRole } from "../../../../shared/models/Settings";
import { IRoleDispersalWeight } from "./../../../../shared/models/GoalDispersal";
let wrapper: ShallowWrapper;
let weightsByRole: IRoleDispersalWeight[] = [];
beforeEach(() => {
weightsByRole = [];
wrapper = shallow(
<DispersalWeightInput
roles={roles}
weightsByRole={weightsByRole}
handleChange={(formData) => (weightsByRole = formData)}
/>
);
});
describe("A User selects a Role", () => {
it("Only allows a role to be selected once", () => {
//Admin will be input row at index 0
//LIC will be input row at index 1
wrapper.setProps({
weightsByRole: [
{ roleName: "Admin", value: 100 },
{ roleName: "LIC", value: 50 }
],
selectedRole: "LIC"
});
wrapper.update();
expect(
(wrapper
.find('[data-test="dispersal-weight-input-row-1"]')
.prop("roles") as IAppRole[]).find((x) => x.name === "Admin")
).toBe(undefined);
expect(
(wrapper
.find('[data-test="dispersal-weight-input-row-0"]')
.prop("roles") as IAppRole[]).find((x) => x.name === "LIC")
).toBe(undefined);
});
it("Allows a appUser to-reselect the currently selected row", () => {
//Admin will be input row at index 0
//LIC will be input row at index 1
wrapper.setProps({
weightsByRole: [
{ roleName: "Admin", value: 100 },
{ roleName: "LIC", value: 50 }
]
});
wrapper.update();
expect(
(wrapper
.find('[data-test="dispersal-weight-input-row-1"]')
.prop("roles") as IAppRole[]).find((x) => x.name === "LIC")
).toEqual({
name: "LIC",
description: "Landmark Investment Center",
permissions: [],
hasCommissions: true,
rank: 1
});
expect(
(wrapper
.find('[data-test="dispersal-weight-input-row-0"]')
.prop("roles") as IAppRole[]).find((x) => x.name === "Admin")
).toEqual({
name: "Admin",
description: "Admin",
permissions: [],
hasCommissions: false,
rank: 99
});
});
it("Updates the correct role name", () => {
wrapper.setProps({
weightsByRole: [
{ roleName: "Admin", value: 100 },
{ roleName: "LIC", value: 50 },
{ roleName: "RegionalManager", value: 100 }
]
});
wrapper.update();
wrapper
.find('[data-test="dispersal-weight-input-row-1"]')
.invoke("setRole")("BM");
expect(weightsByRole.some((x) => x.roleName == "BM")).toBe(true);
expect(weightsByRole.some((x) => x.roleName == "Admin")).toBe(true);
expect(weightsByRole.some((x) => x.roleName == "RegionalManager")).toBe(
true
);
expect(weightsByRole.some((x) => x.roleName == "LIC")).toBe(false);
});
});
describe("A User Updates a RoleWeight", () => {
it("Updates the correct role value", () => {
wrapper.setProps({
weightsByRole: [
{ roleName: "Admin", value: 100 },
{ roleName: "LIC", value: 50 },
{ roleName: "RegionalManager", value: 99 }
]
});
wrapper.update();
wrapper
.find('[data-test="dispersal-weight-input-row-1"]')
.invoke("setValue")(79);
expect(weightsByRole.find((x) => x.roleName == "LIC").value).toBe(79);
expect(weightsByRole.find((x) => x.roleName == "Admin").value).toBe(
100
);
expect(
weightsByRole.find((x) => x.roleName == "RegionalManager").value
).toBe(99);
});
});
describe("A User removes a Row", () => {
it("Removes the Correct Row", () => {
wrapper.setProps({
weightsByRole: [
{ roleName: "Admin", value: 100 },
{ roleName: "LIC", value: 50 },
{ roleName: "RegionalManager", value: 99 }
]
});
wrapper.update();
wrapper
.find('[data-test="dispersal-weight-input-row-1"]')
.invoke("removeRow")();
expect(weightsByRole.length).toBe(2);
expect(weightsByRole.some((x) => x.roleName == "Admin")).toBe(true);
expect(weightsByRole.some((x) => x.roleName == "RegionalManager")).toBe(
true
);
expect(weightsByRole.some((x) => x.roleName == "LIC")).toBe(false);
});
it("Does not allow the last row to be removed", () => {
wrapper.setProps({
weightsByRole: [
{ roleName: "Admin", value: 100 },
{ roleName: "LIC", value: 50 },
{ roleName: "RegionalManager", value: 99 }
]
});
wrapper.update();
expect(
wrapper
.find('[data-test="dispersal-weight-input-row-0"]')
.prop("disableDelete")
).toBe(false);
expect(
wrapper
.find('[data-test="dispersal-weight-input-row-1"]')
.prop("disableDelete")
).toBe(false);
expect(
wrapper
.find('[data-test="dispersal-weight-input-row-2"]')
.prop("disableDelete")
).toBe(false);
wrapper.setProps({
weightsByRole: [{ roleName: "RegionalManager", value: 99 }]
});
wrapper.update();
expect(
wrapper
.find('[data-test="dispersal-weight-input-row-0"]')
.prop("disableDelete")
).toBe(true);
});
});
const roles: IAppRole[] = [
{
name: "Admin",
description: "Admin",
permissions: [],
hasCommissions: false,
rank: 99
},
{
name: "LIC",
description: "Landmark Investment Center",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "RegionalManager",
description: "Regional Manager",
permissions: [],
hasCommissions: true,
rank: 97
},
{
name: "BM",
description: "Branch Manager",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "ABM",
description: "Assistant Branch Manager",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "BOM",
description: "Branch Operations Manager",
permissions: [],
hasCommissions: true,
rank: 1
},
{
name: "PFO",
description: "PERS FIN OFFICER",
permissions: [],
hasCommissions: true,
rank: 1
}
];
@@ -0,0 +1,84 @@
import _ from "lodash";
import React, { useEffect } from "react";
import { IRoleDispersalWeight } from "../../../../shared/models/GoalDispersal";
import { IAppRole } from "../../../../shared/models/Settings";
import DispersalWeightInputRow from "./DispersalWeightInputRow";
export interface IDispersalWeightInputFormProps {
roles: IAppRole[];
weightsByRole: IRoleDispersalWeight[];
handleChange: (fromData: IRoleDispersalWeight[]) => void;
}
const DispersalWeightInput: React.FC<IDispersalWeightInputFormProps> = (
props
) => {
const determineRolesToDisplay = (currentRoleName: string): IAppRole[] => {
const currentRole = props.roles.find(
(x) => x.name.toLowerCase() === currentRoleName.toLowerCase()
);
if (currentRole !== undefined) {
return [currentRole, ...unselectedRoles()];
} else {
return [...unselectedRoles()];
}
};
const unselectedRoles = (): IAppRole[] => {
return props.roles.filter(
(role) =>
props.weightsByRole
.map((existing) => existing.roleName)
.find((roleName) => role.name === roleName) === undefined
);
};
const updateRowRole = (index: number, newRoleName: string) => {
const newRole = props.roles.find(
(x) => x.name.toLowerCase() === newRoleName.toLowerCase()
);
if (newRole === undefined) {
return;
}
const newWeightsByRole = [...props.weightsByRole];
newWeightsByRole[index].roleName = newRole.name;
props.handleChange(newWeightsByRole);
};
const updateRowValue = (index: number, newValue: number) => {
const newWeightsByRole = [...props.weightsByRole];
newWeightsByRole[index].value = newValue;
props.handleChange(newWeightsByRole);
};
const removeRow = (index: number) => {
const newWeightsByRole = [...props.weightsByRole];
newWeightsByRole.splice(index, 1);
props.handleChange(newWeightsByRole);
};
return (
<div>
{props.weightsByRole.map((roleWeight, i) => {
return (
<DispersalWeightInputRow
data-test={`dispersal-weight-input-row-${i}`}
key={i}
rowIndex={i}
disableDelete={props.weightsByRole.length <= 1}
roles={determineRolesToDisplay(roleWeight.roleName)}
selectedRole={roleWeight.roleName}
percentage={roleWeight.value}
removeRow={() => {
removeRow(i);
}}
setRole={(role) => updateRowRole(i, role)}
setValue={(percent) => updateRowValue(i, percent)}
/>
);
})}
</div>
);
};
export default DispersalWeightInput;
@@ -0,0 +1,110 @@
import React from "react";
import {
Button,
Col,
FormFeedback,
FormGroup,
Input,
InputGroup,
InputGroupAddon,
Row
} from "reactstrap";
import { IAppRole } from "../../../../shared/models/Settings";
import Translate from "../../../../translate/Translate";
export interface IDispersalWeightInputRowProps {
rowIndex: number;
roles: IAppRole[];
selectedRole: string;
percentage: number;
removeRow: () => void;
setRole: (role: string) => void;
setValue: (newValue: number) => void;
disableDelete: boolean;
}
const _translate =
Translate.Get.components.goal.goalDispersal.customWeightedDispersal
.dispersalWeightInputRow;
const DispersalWeightInputRow: React.FC<IDispersalWeightInputRowProps> = (
props
) => {
return (
<Row>
<Col>
<FormGroup>
<label hidden={props.rowIndex > 0}>
{_translate.roleInput.label}
<span className="text-danger">*</span>
</label>
<Input
name="ActivityProduct"
type="select"
bsSize="sm"
required
invalid={props.selectedRole === ""}
onChange={(event) => props.setRole(event.target.value)}
value={props.selectedRole}
>
<option
key=""
value=""
hidden={props.selectedRole != ""}
>
{_translate.roleInput.placeholder}
</option>
{props.roles.map((role, i) => {
return (
<option key={role.name} value={role.name}>
{role.name}
</option>
);
})}
</Input>
<FormFeedback>
{_translate.roleInput.validation}
</FormFeedback>
</FormGroup>
</Col>
<Col>
<FormGroup>
<label hidden={props.rowIndex > 0}>
{_translate.valueInput.label}
<span className="text-danger">*</span>
</label>
<InputGroup>
<Input
sty
type="number"
name="ActivityProduct"
bsSize="sm"
required
invalid={
props.percentage < 0 || props.percentage > 100
}
onChange={(event) =>
props.setValue(parseInt(event.target.value))
}
value={props.percentage}
></Input>
<InputGroupAddon addonType="append">
<Button
disabled={props.disableDelete}
size="sm"
onClick={() => props.removeRow()}
color={props.disableDelete ? "dark" : "danger"}
>
&times;
</Button>
</InputGroupAddon>
<FormFeedback>
{_translate.valueInput.validation}
</FormFeedback>
</InputGroup>
</FormGroup>
</Col>
</Row>
);
};
export default DispersalWeightInputRow;