Adding
This commit is contained in:
+48
@@ -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;
|
||||
+121
@@ -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;
|
||||
+80
@@ -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;
|
||||
+65
@@ -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);
|
||||
});
|
||||
});
|
||||
+150
@@ -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);
|
||||
});
|
||||
});
|
||||
+164
@@ -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;
|
||||
+102
@@ -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;
|
||||
+122
@@ -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");
|
||||
});
|
||||
});
|
||||
+42
@@ -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;
|
||||
+307
@@ -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[];
|
||||
+188
@@ -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;
|
||||
+228
@@ -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
|
||||
}
|
||||
];
|
||||
+84
@@ -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;
|
||||
+110
@@ -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"}
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
<FormFeedback>
|
||||
{_translate.valueInput.validation}
|
||||
</FormFeedback>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
export default DispersalWeightInputRow;
|
||||
Reference in New Issue
Block a user