This commit is contained in:
Zeeshaun
2026-03-17 11:09:46 -05:00
parent bd1a9744ad
commit 6e7edcfe1b
332 changed files with 64174 additions and 0 deletions
@@ -0,0 +1,120 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import * as adalConfig from "../../adalConfig";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AccountPageHeader from "./AccountPageHeader";
describe("AccountPageHeader", () => {
var spy: jest.SpyInstance;
let wrapper: ShallowWrapper;
const selectedAccount: IRepoAccount = {
repoId: 476,
origAccountTypeCode: "",
accountNo: "123456",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 0,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
currentDelinquentAmount: 19.45,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [{
repoId: 1,
individualId: "1",
ownershipType: 6,
individualName: "John Doe",
}],
collaterals: [],
repoStatusChanges: [],
};
let updatedBackButtonValue: boolean;
beforeEach(() => {
updatedBackButtonValue = false;
wrapper = shallow(
<AccountPageHeader
accountNoHeader="abcd1234"
selectedRepoAccount={selectedAccount}
allowedStatusChanges={[]}
onStatusChange={() => {}}
onBackButtonClicked={() => updatedBackButtonValue = true}
putRepoAccount={()=>{}}
/>
);
spy = jest.spyOn(adalConfig, "hasAdminRole").mockReturnValueOnce(false).mockReturnValueOnce(true);
});
it ("Renders Member Name", () => {
const memberName = wrapper.find(`[data-test='account-page-header-member-name']`);
expect(memberName.length).toBe(1);
expect(memberName.text()).toEqual("John Doe");
});
it ("Renders Missing When There is No Selected Account", () => {
wrapper.setProps({
selectedRepoAccount: undefined,
});
const memberName = wrapper.find(`[data-test='account-page-header-member-name']`);
expect(memberName.length).toBe(1);
expect(memberName.text()).toEqual("Missing");
});
it ("Back Button Callback Works", () => {
const backBtn = wrapper.find(`[data-test='account-page-header-back-btn']`);
expect(backBtn.length).toBe(1);
expect(updatedBackButtonValue).toBe(false);
backBtn.simulate("click");
expect(updatedBackButtonValue).toBe(true);
});
it ("Does Not Render \"(Closed)\" Next to Name When Account is Open", () => {
const closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
expect(closedAccountLabel.length).toBe(0);
});
it ("Renders \"(Closed)\" Next to Name When Account is Closed", () => {
wrapper = wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
dateClosed: moment("1941-12-07")
}
});
var closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
expect(closedAccountLabel.length).toBe(1);
closedAccountLabel = wrapper.find(`[data-test='reopen-button']`);
expect(closedAccountLabel.length).toBe(0);
});
it ("Renders \"Repopen\" button Next to Name When Account is Closed and user is Admin", () => {
wrapper = wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
dateClosed: moment("1941-12-07")
}
});
var closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
expect(closedAccountLabel.length).toBe(1);
closedAccountLabel = wrapper.find(`[data-test='reopen-button']`);
expect(closedAccountLabel.length).toBe(1);
});
});
@@ -0,0 +1,78 @@
import React from 'react';
import { IAllowedStatusChange } from '../../reducers/SettingsReducer';
import StatusChangeSelector from '../status-change-selector/StatusChangeSelector';
import RepoAccountService from '../../services/RepoAccountService';
import { IRepoAccount } from '../../services/service-types/RepoAccountTypes';
import * as adalConfig from "../../adalConfig";
const cloneDeep = require("lodash.clonedeep");
interface AccountPageHeaderProps {
accountNoHeader: string;
selectedRepoAccount?: IRepoAccount;
allowedStatusChanges: IAllowedStatusChange[];
onBackButtonClicked: () => any;
onStatusChange: (status: string) => any;
putRepoAccount: (repoAccount: IRepoAccount) => any;
}
const AccountPageHeader: React.FC<AccountPageHeaderProps> = (props) => {
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedRepoAccount);
const reopenAccountClicked = () => {
const shouldReOpenAccount = window.confirm("Are you sure you want to reopen this account? This can have unexpected side effects. Use Caution with accounts that have already been refunded.");
if(shouldReOpenAccount) {
reopenClosedAccount();
}
}
const reopenClosedAccount = () => {
if (!props.selectedRepoAccount) {
console.warn("Cannot close account when the Selected Account is not loaded. Returning.");
return;
}
const copiedRepoAccount = cloneDeep(props.selectedRepoAccount) as IRepoAccount;
copiedRepoAccount.dateClosed = undefined;
props.putRepoAccount(copiedRepoAccount);
}
return (
<div className={'back-navigation-header' + (isAccountClosed ? ' header-closed' : '')}>
<div className="row form-inline">
<div className="col-sm">
<h5>
<i className="fa fa-chevron-left text-primary" aria-hidden="true" onClick={props.onBackButtonClicked} data-test="account-page-header-back-btn"></i>
Account: { props.accountNoHeader }
{ isAccountClosed ?
(
<span className="text-danger" data-test="closed-account-label"> (CLOSED)</span>
)
: null}
{ isAccountClosed && adalConfig.hasAdminRole() ?
(
<button className="form-input btn btn-danger btn-sm ml-2" data-test="reopen-button" onClick={() => reopenAccountClicked()}>Reopen This Account</button>
)
: null }
</h5>
</div>
<div className="col-sm">
<p><b data-test="account-page-header-member-name">{RepoAccountService.GetMemberName(props.selectedRepoAccount)}</b></p>
</div>
<div className="col-sm">
<StatusChangeSelector
allowedStatusChanges={props.allowedStatusChanges}
onStatusChange={props.onStatusChange}
selectedRepoAccount={props.selectedRepoAccount}
/>
</div>
</div>
<hr></hr>
</div>
);
}
export default AccountPageHeader;
@@ -0,0 +1,8 @@
.alerts-list {
max-height: 11em;
overflow-y: scroll;
}
.alert-box .card-header {
padding: .5em;
}
@@ -0,0 +1,140 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import ReactDOM from "react-dom";
import { IAlert } from "../../reducers/AlertReducer";
import AlertBox, { groupAlerts, IAlertBoxProps } from "./AlertBox";
it("renders without crashing", () => {
const div = document.createElement("div");
const props: IAlertBoxProps = { alerts: [], title: "title", alertRowClicked: () => {}};
ReactDOM.render(<AlertBox {...props} />, div);
ReactDOM.unmountComponentAtNode(div);
});
const findByTestAttribute = (wrapper: ShallowWrapper, attr: string) => {
const component = wrapper.find(`[data-test='${attr}']`);
return component;
};
describe("AlertBox Component", () => {
const props: IAlertBoxProps = { alerts: [], title: "title", alertRowClicked: () => {}};
let wrapper = shallow(<AlertBox {...props} />);
const first: IAlert = { id: 1, repoId: 2, accountNo: "2", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Message", dayCount: 60 };
const second: IAlert = { id: 2, repoId: 3, accountNo: "2", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Jane Sample", markedReadBy: "", message: "Message", dayCount: 60 };
const third: IAlert = { id: 3, repoId: 4, accountNo: "3", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Other Message", dayCount: 3 };
const fourth: IAlert = { id: 4, repoId: 5, accountNo: "4", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Other Message", dayCount: 3 };
it ("Should render without errors", () => {
const component = findByTestAttribute(wrapper, "alertBoxComponent");
expect(component.length).toBe(1);
});
it ("Should render a header", () => {
const component = findByTestAttribute(wrapper, "alertBoxHeader");
expect(component.length).toBe(1);
});
it ("Should render an alerts list", () => {
const component = findByTestAttribute(wrapper, "alertsList");
expect(component.length).toBe(1);
});
it("groups an empty alert array", () => {
// arrange
const alerts: IAlert[] = [];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(0);
});
it("groups one alert", () => {
// arrange
const alerts: IAlert[] = [first];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(1);
const group = result[0];
expect(group.message).toBe("Message");
expect(group.repoIds[0]).toBe(2);
});
it("groups two alerts of the same type", () => {
// arrange
const alerts: IAlert[] = [first, second];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(1);
const group = result[0];
expect(group.message).toBe("Message");
expect(group.repoIds[0]).toBe(2);
expect(group.repoIds[1]).toBe(3);
});
it("groups two alerts of different types", () => {
// arrange
const alerts: IAlert[] = [first, third];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(2);
const messageGroup = result[0];
expect(messageGroup.message).toBe("Message");
expect(messageGroup.repoIds[0]).toBe(2);
const otherGroup = result[1];
expect(otherGroup.message).toBe("Other Message");
expect(otherGroup.repoIds[0]).toBe(4);
});
it("discards duplicate alerts", () => {
// arrange
const alerts: IAlert[] = [first, second, first];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(1);
const group = result[0];
expect(group.message).toBe("Message");
expect(group.repoIds[0]).toBe(2);
expect(group.repoIds[1]).toBe(3);
});
it("sorts alert groups in descending order of alert count", () => {
// arrange
const alerts: IAlert[] = [first, third, fourth];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(2);
const otherGroup = result[0];
expect(otherGroup.message).toBe("Other Message");
expect(otherGroup.repoIds[0]).toBe(4);
expect(otherGroup.repoIds[1]).toBe(5);
const messageGroup = result[1];
expect(messageGroup.message).toBe("Message");
expect(messageGroup.repoIds[0]).toBe(2);
});
});
@@ -0,0 +1,66 @@
import React from "react";
import { IAlert } from "../../reducers/AlertReducer";
import IAlertGroup from "../../shared/types/IAlertGroup";
import AlertRow from "./../alert-row/AlertRow";
import "./AlertBox.css";
export interface IAlertBoxProps {
alerts: IAlert[];
currentlySelectedAlertMessage?: string;
title: string;
alertRowClicked: (message: string, repoIds: number[]) => any;
}
const AlertBox: React.FC<IAlertBoxProps> = (props: IAlertBoxProps) => {
const groups = groupAlerts(props.alerts);
return (
<div className="card h-100 alert-box" data-test="alertBoxComponent">
<h6 className="card-header" data-test="alertBoxHeader">
{totalAlertCount(groups)} {props.title}
</h6>
<div className="">
<ul className="alerts-list list-group list-group-flush" data-test="alertsList">
{groups.map((value, key) => {
return (
<AlertRow
key={key}
message={value.message}
currentlySelectedAlertMessage={props.currentlySelectedAlertMessage}
repoIds={value.repoIds}
alertRowClicked={props.alertRowClicked}
/>
);
})}
</ul>
</div>
</div>
);
};
const totalAlertCount = (groups: IAlertGroup[]) => {
return groups.reduce((total, group) => total + group.repoIds.length, 0);
};
export const groupAlerts = (alerts: IAlert[]) => {
let groups: IAlertGroup[] = [];
for (let alert of alerts) {
let found = groups.find(x => x.message === alert.message);
if (!found) {
var group: IAlertGroup = {message: alert.message, repoIds: [alert.repoId]};
groups.push(group);
} else {
if (!found.repoIds.includes(alert.repoId)){
found.repoIds.push(alert.repoId);
}
}
}
return groups.sort((a, b) => {
return b.repoIds.length - a.repoIds.length;
});
};
export default AlertBox;
@@ -0,0 +1,13 @@
.alert-row {
cursor: pointer;
font-size: .9em;
padding: .5em 1em;
}
.alert-row.selected-row {
background-color: #d9ebff;
}
.alert-row:hover:not(.selected-row) {
background-color: #f5f5f5;
}
@@ -0,0 +1,90 @@
import { AssertionError } from "assert";
import { shallow, ShallowWrapper } from "enzyme";
import React, { Component } from "react";
import AlertRow from "./AlertRow";
const findByTestAttribute = (component: ShallowWrapper, attr: string) => {
const wrapper = component.find(`[data-test='${attr}']`);
return wrapper;
};
describe("AlertRow", () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[]}
alertRowClicked={(_: string, __: number[]) => {}}
/>
);
});
it ("Should render without errors", () => {
const component = findByTestAttribute(wrapper, "alertRowComponent");
expect(component.length).toBe(1);
});
it ("Should render an alert type", () => {
const component = findByTestAttribute(wrapper, "alertType");
expect(component.length).toBe(1);
});
it ("Should render a type count", () => {
const component = findByTestAttribute(wrapper, "typeCount");
expect(component.length).toBe(1);
});
it("Should Render as badge-primary with count under 5", () => {
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3]}
alertRowClicked={(_: string, __: number[]) => {}}
/>);
const component = findByTestAttribute(tempwrapper, "typeCount");
expect(component.hasClass("badge-primary")).toBe(true);
});
it("Should Render as badge-warning with count > 6 and count < 20", () => {
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3,4,5,6,7,8]}
alertRowClicked={(_: string, __: number[]) => {}}
/>);
const component = findByTestAttribute(tempwrapper, "typeCount");
expect(component.hasClass("badge-warning")).toBe(true);
});
it("Should Render as badge-danger when count is greater than 20", () => {
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]}
alertRowClicked={(_: string, __: number[]) => {}}
/>);
const component = findByTestAttribute(tempwrapper, "typeCount");
expect(component.hasClass("badge-danger")).toBe(true);
});
it("alert row properly handles click", () => {
var clicked = false;
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]}
alertRowClicked={(_: string, __: number[]) => {clicked=true}}
/>);
const component = findByTestAttribute(tempwrapper, "alertRowComponent");
component.simulate("click");
expect(clicked).toBe(true);
});
});
@@ -0,0 +1,39 @@
import React from "react";
import "./AlertRow.css";
interface IAlertRowProps {
message: string;
repoIds: number[];
currentlySelectedAlertMessage?: string;
alertRowClicked: (message: string, repoIds: number[]) => any;
}
const AlertRow: React.FC<IAlertRowProps> = (props: IAlertRowProps) => {
const rowClicked = () => {
props.alertRowClicked(props.message, props.repoIds);
};
const getCountClassStyle = (count: number): string => {
if (count < 5) {
return "badge-primary";
}
if (count < 20) {
return "badge-warning";
}
return "badge-danger";
};
return (
<li
className={"alert-row list-group-item d-flex justify-content-between align-items-center " + (props.currentlySelectedAlertMessage === props.message ? " selected-row" : "")}
data-test="alertRowComponent"
onClick={rowClicked}
>
<span data-test="alertType">{props.message}</span>
<span className={"badge badge-pill " + getCountClassStyle(props.repoIds.length)} data-test="typeCount">{props.repoIds.length}</span>
</li>
);
};
export default AlertRow;
@@ -0,0 +1,9 @@
.auction-assignment-section input,
.auction-assignment-section select,
.auction-assignment-section textarea {
margin-bottom: 0.5em;
}
.auction-assignment-section p {
margin-bottom: 0;
}
@@ -0,0 +1,181 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import { ICleaningLevel } from "../../reducers/SettingsReducer";
import AuctionAssignment, { IUserSelectedAuctionAssignment } from "./AuctionAssignment";
// - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
describe("Auction Collateral Location Component", () => {
let wrapper: ShallowWrapper;
const dummyAuctionAssignment: IUserSelectedAuctionAssignment = { repoId: 1 };
const dummyCleaningLevels: ICleaningLevel[] = [
{
code: 1,
description: "Super Clean",
},
{
code: 2,
description: "Dirty",
},
];
const dummyAuctionHouses: IAuctionHouse[] = [
{
auctionID: "1060",
name: "Dealers Auto Auction Southwest",
active: "yes",
address_Line_1: "1433 South 19th Avenue",
address_Line_2: "-",
city: "Phoenix",
state: "AZ",
zip: 85009,
country: "USA",
phone: "(602) 253-7766",
fax: "(602) 253-4646",
latitude: 33.4337272644043,
longitude: -112.09916687011719
},
{
auctionID: "1061",
name: "ADESA Portland",
active: "yes",
address_Line_1: "23585 NE Sandy Blvd.",
address_Line_2: "-",
city: "Wood Village",
state: "OR",
zip: 97060,
country: "USA",
phone: "(800) 300-3200",
fax: "(503) 661-7286",
latitude: 45.545101165771484,
longitude: -122.41893005371094
},
];
let dummyAuctionAssignmentToUpdate: IUserSelectedAuctionAssignment = { repoId: 1 };
const dummyUpdateAuctionAssignment = (auctionAssignment: IUserSelectedAuctionAssignment) => {
dummyAuctionAssignmentToUpdate = auctionAssignment;
};
beforeEach(() => {
wrapper = shallow(
<AuctionAssignment
auctionHouses={dummyAuctionHouses}
cleaningLevels={dummyCleaningLevels}
auctionAssignment={dummyAuctionAssignment}
onAuctionAssignmentChange={dummyUpdateAuctionAssignment}
collateralLocation={{} as any}
/>
);
});
afterEach(() => {
dummyAuctionAssignmentToUpdate = {
repoId: 1
};
});
const getCardView = () => {
return wrapper.find("CardView").dive();
};
it("Renders & Updates Auction House", () => {
// Inital value is present
let auctionHouseSelection = getCardView().find(`[data-test='auction-assignment-auction-house']`);
let auctionHouseAddress = getCardView().find(`[data-test='auction-assignment-auction-house-address']`);
expect(auctionHouseSelection.length).toBe(1);
expect(auctionHouseSelection.props().value).toBe(undefined);
expect(auctionHouseAddress.length).toBe(1);
expect(auctionHouseAddress.props().value).toEqual("Select Auction House for Address");
// Change triggers callback
auctionHouseSelection.simulate("change", "1060");
expect(dummyAuctionAssignmentToUpdate.auctionId).toEqual(dummyAuctionHouses[0].auctionID);
// Change does not alter original prop object
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
// Prop changes update input
wrapper = wrapper.setProps({
auctionAssignment: {
auctionId: dummyAuctionHouses[1].auctionID,
},
});
auctionHouseSelection = getCardView().find(`[data-test='auction-assignment-auction-house']`);
auctionHouseAddress = getCardView().find(`[data-test='auction-assignment-auction-house-address']`);
expect(auctionHouseSelection.props().value).toBe("1061");
expect(auctionHouseAddress.props().value).toEqual("23585 NE Sandy Blvd. -, Wood Village, OR, USA 97060");
});
it("Renders & Updates Cleaning Level", () => {
// Inital value is present
let cleaningLevelSelection = getCardView().find(`[data-test='auction-assignment-cleaning-level']`);
expect(cleaningLevelSelection.length).toBe(1);
expect(cleaningLevelSelection.props().value).toBeUndefined();
// Change triggers callback
cleaningLevelSelection.simulate("change", 1);
expect(dummyAuctionAssignmentToUpdate.cleaningLevel).not.toBeUndefined();
expect(dummyAuctionAssignmentToUpdate.cleaningLevel).toEqual(dummyCleaningLevels[0].code);
// Change does not alter original prop object
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
// Prop changes update input
wrapper = wrapper.setProps({
auctionAssignment: {
cleaningLevel: dummyCleaningLevels[1].code,
},
});
cleaningLevelSelection = getCardView().find(`[data-test='auction-assignment-cleaning-level']`);
expect(cleaningLevelSelection.props().value).toBe(2);
});
it("Renders & Updates Auction Notes", () => {
// Inital value is present
let additionalNotesInput = getCardView().find(`[data-test='auction-assignment-pickup-notes']`);
expect(additionalNotesInput.length).toBe(1);
expect(additionalNotesInput.props().value).toBeUndefined();
// Change triggers callback
additionalNotesInput.simulate("change", "Some New Notes");
expect(dummyAuctionAssignmentToUpdate.auctionNotes).not.toBeUndefined();
expect(dummyAuctionAssignmentToUpdate.auctionNotes).toEqual("Some New Notes");
// Change does not alter original prop object
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
// Prop changes update input
wrapper = wrapper.setProps({
auctionAssignment: {
auctionNotes: "Even newer Notes!",
},
});
additionalNotesInput = getCardView().find(`[data-test='auction-assignment-pickup-notes']`);
expect(additionalNotesInput.props().value).toBe("Even newer Notes!");
});
it("Sets Default Legal Sale Date When Legal Sale Date is Undefined", () => {
wrapper.setProps({
defaultLegalSaleDate: moment("1969-07-20")
});
expect(dummyAuctionAssignmentToUpdate.legalSaleDate).toEqual(moment("1969-07-20"));
});
it("Does Not Set Default Legal Sale Date When Legal Sale Date is Not Undefined", () => {
wrapper.setProps({
defaultLegalSaleDate: moment("1969-07-20"),
auctionAssignment: {
legalSaleDate: moment("1957-10-04")
}
});
expect(dummyAuctionAssignmentToUpdate.legalSaleDate).toEqual(undefined);
});
});
@@ -0,0 +1,144 @@
import moment, { Moment } from "moment";
import React from "react";
import { ICollateralLocation } from "../../containers/auction-collateral-location/AuctionCollateralLocation";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import { ICleaningLevel } from "../../reducers/SettingsReducer";
import CardView from "../card-view/CardView";
import FormInput, { FormInputType } from "../form-input/FormInput";
import AuctionAssignmentMap from "./auction-assignment-map/AuctionAssignmentMap";
import "./AuctionAssignment.css";
const cloneDeep = require("lodash.clonedeep");
export interface IUserSelectedAuctionAssignment {
repoId: number;
auctionId?: string;
cleaningLevel?: number;
availablePickupDate?: Moment;
legalSaleDate?: Moment;
auctionNotes?: string;
}
export interface IAuctionAssignmentProps {
collateralLocation: ICollateralLocation;
auctionAssignment: IUserSelectedAuctionAssignment;
auctionHouses: IAuctionHouse[];
cleaningLevels: ICleaningLevel[];
defaultLegalSaleDate?: Moment;
onAuctionAssignmentChange: (auctionAssignment: IUserSelectedAuctionAssignment) => any;
}
type IUserAuctionAssignmentUpdater = (x: IUserSelectedAuctionAssignment) => void;
const AuctionAssignment: React.FC<IAuctionAssignmentProps> = (props) => {
const applyChange = (updater: IUserAuctionAssignmentUpdater) => {
const copiedAuctionassignment = cloneDeep(props.auctionAssignment);
updater(copiedAuctionassignment);
props.onAuctionAssignmentChange(copiedAuctionassignment);
};
const getAuctionHouseAddressDisplay = (auctionId?: string) => {
if (!auctionId) {
return "Select Auction House for Address";
}
const auctionHouse = props.auctionHouses.find((auction) => auction.auctionID === auctionId );
if (auctionHouse) {
return `${auctionHouse.address_Line_1} ${auctionHouse.address_Line_2}, ${auctionHouse.city}, ${auctionHouse.state}, ${auctionHouse.country} ${auctionHouse.zip || ""}`;
}
};
const setDefaultLegalSaleValue = (defaultLegalSale?: Moment) => {
if (!props.auctionAssignment.legalSaleDate && defaultLegalSale) {
applyChange(auctionAssignment => auctionAssignment.legalSaleDate = defaultLegalSale);
}
};
setDefaultLegalSaleValue(props.defaultLegalSaleDate);
return (
<CardView
header="Auction Assignment"
rows={[
<div key="auction-assignment-form" className="auction-assignment-section container">
<div className="row">
<div className="col-md">
<FormInput
label="Auction House"
type={FormInputType.Selection}
value={props.auctionAssignment.auctionId}
onChange={(auctionId) => applyChange(auctionAssignment => auctionAssignment.auctionId = auctionId)}
required={true}
placeholder="Select Auction House"
options={props.auctionHouses.map((auctionHouse) => {
return {
value: auctionHouse.auctionID,
display: auctionHouse.name
};
})}
data-test="auction-assignment-auction-house"
/>
<input
className="form-control"
value={getAuctionHouseAddressDisplay(props.auctionAssignment.auctionId)}
data-test="auction-assignment-auction-house-address"
disabled={true}
/>
<hr/>
<FormInput
label="Cleaning Level"
type={FormInputType.Selection}
value={props.auctionAssignment.cleaningLevel}
onChange={(cleaningLevel) => applyChange(auctionAssignment => auctionAssignment.cleaningLevel = parseInt(cleaningLevel))}
placeholder="Select Cleaning Level"
required={true}
options={props.cleaningLevels.map((cleaningLevel) => {
return {
value: cleaningLevel.code,
display: cleaningLevel.description
};
})}
data-test="auction-assignment-cleaning-level"
/>
<FormInput
label="Available For Pickup"
type={FormInputType.Datetime}
minDate={moment()}
value={props.auctionAssignment.availablePickupDate}
required={true}
onChange={(availablePickupDate) => applyChange(auctionAssignment => auctionAssignment.availablePickupDate = availablePickupDate)}
data-test="auction-assignment-available-for-pickup"
/>
<FormInput
label="Legal Sale"
type={FormInputType.Datetime}
minDate={moment()}
value={props.auctionAssignment.legalSaleDate}
required={true}
onChange={(legalSaleDate) => applyChange(auctionAssignment => auctionAssignment.legalSaleDate = legalSaleDate)}
data-test="auction-assignment-legal-sale"
/>
<FormInput
label="Additional Pickup Notes"
type={FormInputType.Textarea}
value={props.auctionAssignment.auctionNotes}
onChange={(auctionNotes) => applyChange(auctionAssignment => auctionAssignment.auctionNotes = auctionNotes)}
data-test="auction-assignment-pickup-notes"
/>
</div>
<div className="col-md">
<AuctionAssignmentMap
collateralLocation={props.collateralLocation}
currentlySelectedAuctionId={props.auctionAssignment.auctionId}
auctionHouses={props.auctionHouses}
onPinSelected={(auctionId) => applyChange(auctionAssignment => auctionAssignment.auctionId = auctionId)}
data-test=""
/>
</div>
</div>
</div>
]}
/>
);
};
export default AuctionAssignment;
@@ -0,0 +1,9 @@
#auction-house-location-map {
width: 100%;
height: 100%;
min-height: 320px;
}
#collateral-center-btn {
margin-top: 0.5em;
}
@@ -0,0 +1,11 @@
import { shallow, ShallowWrapper, mount} from "enzyme";
import moment from "moment";
import React from "react";
import AuctionAssignmentMap from "./AuctionAssignmentMap";
//import AuctionAssignment, { IUserSelectedAuctionAssignment } from "./AuctionAssignment";
//TODO: This one is hard, come back to this later
describe("Auction Assignment Map Component", () => {
it("Does Nothing For Now", () => {});
});
@@ -0,0 +1,244 @@
import React from "react";
import * as atlas from 'azure-maps-control';
import { toast } from "react-toastify";
import { ICollateralLocation } from "../../../containers/auction-collateral-location/AuctionCollateralLocation";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
import { ILocationCoordinates } from "../../../services/service-types/LocationTypes";
import NetworkService from "../../../services/NetworkService";
import Configuration from "../../../shared/configuration/Configuration";
import ToastDefaults from "../../../shared/notifications/ToastDefaults";
import "./AuctionAssignmentMap.css";
//const Microsoft = (window as any).Microsoft;
var map: atlas.Map;
var popup = new atlas.Popup({
pixelOffset: [0, -18],
closeButton: false
});
interface IAuctionAssignmentMapProps {
collateralLocation: ICollateralLocation;
auctionHouses: IAuctionHouse[];
currentlySelectedAuctionId?: string;
onPinSelected: (auctionId: string) => any;
}
interface ICoordinates {
lat: number;
long: number;
}
interface IAuctionAssignmentMapState {
collateralCoordinates?: ICoordinates;
}
class AuctionAssignmentMap extends React.Component<IAuctionAssignmentMapProps, IAuctionAssignmentMapState> {
constructor(props: IAuctionAssignmentMapProps) {
super(props);
this.state = {};
this.LocationsApiRecieved = this.LocationsApiRecieved.bind(this);
this.centerMap = this.centerMap.bind(this);
}
componentDidMount() {
this.instantiateMap();
}
componentDidUpdate(prevProps: IAuctionAssignmentMapProps, prevState: IAuctionAssignmentMapState) {
if (this.state.collateralCoordinates && prevState.collateralCoordinates !== this.state.collateralCoordinates) {
this.updateMap(true);
}
if (prevProps.currentlySelectedAuctionId !== this.props.currentlySelectedAuctionId) {
this.updateMap();
}
}
centerMap() {
if (this.isCollateralLocationFilled()) {
const addressParam = encodeURIComponent(this.props.collateralLocation.address);
const zipParam = encodeURIComponent(this.props.collateralLocation.zip);
const url = `${Configuration.Get.baseUrls.locationGeocode}api/Location/GetClosestPositionByAddress?Street=${addressParam}&ZipCode=${zipParam}`;
this.sendLocationsApiRequest(url);
}
}
isCollateralLocationFilled() {
return !!this.props.collateralLocation.address && !!this.props.collateralLocation.zip;
}
sendLocationsApiRequest(url: string) {
NetworkService.FetchLocationData(url)
.then((data) => {
this.LocationsApiRecieved(data);
})
.catch((error) => {
const message = "Could not locate collateral address";
window.console.warn(message, error);
toast.warn(message, ToastDefaults.baseParams);
});
}
LocationsApiRecieved(data: ILocationCoordinates) {
this.setState({
collateralCoordinates: {
lat: data.latitude,
long: data.longitude
}
});
}
getAuctionHouse(auctionId?: string) {
return this.props.auctionHouses.find((auctionHouse) => auctionHouse.auctionID === auctionId);
}
updateMap(switchViewToCollateral: boolean = false) {
popup.close()
const selectedAuctionHouse = this.getAuctionHouse(this.props.currentlySelectedAuctionId);
let viewPortLocation: any;
if (this.state.collateralCoordinates && switchViewToCollateral) {
viewPortLocation = atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat);//test this Corey
} else if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
viewPortLocation = atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude, selectedAuctionHouse.latitude);
} else {
viewPortLocation = atlas.data.Position.fromLatLng(
Configuration.Get.defaultAuctionAssignmentMapLocation.longitude,
Configuration.Get.defaultAuctionAssignmentMapLocation.latitude,
);
}
map.markers.clear();
map.setCamera({
center: viewPortLocation
})
var validPins = [];
// Push Collateral & Selected Auction Pins First
// This will ensure that they appear on top
if (this.state.collateralCoordinates) {
validPins.push(new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat),
color: "green"
}));
}
if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
const selectedAuctionHouse = this.props.auctionHouses.find((auctionHouse) => auctionHouse.auctionID === this.props.currentlySelectedAuctionId);
if (selectedAuctionHouse) {
const pushPin = new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude || 0, selectedAuctionHouse.latitude || 0),//Check this Corey
color: "red"
});
validPins.push(pushPin);
map.events.add("mouseover", pushPin, () => this.openPopup(selectedAuctionHouse, popup, map));
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave
}
}
for (let i = 0; i < this.props.auctionHouses.length; i++) {
const auctionHouse = this.props.auctionHouses[i];
if (auctionHouse.latitude && auctionHouse.longitude && this.props.currentlySelectedAuctionId !== auctionHouse.auctionID) {
const pushPin = new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(auctionHouse.longitude, auctionHouse.latitude),
});
validPins.push(pushPin);
map.events.add("mouseover", pushPin, () => this.openPopup(auctionHouse, popup, map));
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave)
map.events.add("click", pushPin, () => this.props.onPinSelected(auctionHouse.auctionID));
}
}
map.markers.add(validPins);
}
instantiateMap(switchViewToCollateral: boolean = false) {
const selectedAuctionHouse = this.getAuctionHouse(this.props.currentlySelectedAuctionId);
let viewPortLocation: any;
if (this.state.collateralCoordinates && switchViewToCollateral) {
viewPortLocation = atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat);//test this Corey
} else if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
viewPortLocation = atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude, selectedAuctionHouse.latitude);
} else {
viewPortLocation = atlas.data.Position.fromLatLng(
Configuration.Get.defaultAuctionAssignmentMapLocation.longitude,
Configuration.Get.defaultAuctionAssignmentMapLocation.latitude,
);
}
map = new atlas.Map("auction-house-location-map", {
zoom: 9,
center: viewPortLocation,
authOptions: {
authType: atlas.AuthenticationType.subscriptionKey,
subscriptionKey: Configuration.Get.atlasCredentials
}
});
var validPins = [];
// Push Collateral & Selected Auction Pins First
// This will ensure that they appear on top
if (this.state.collateralCoordinates) {
validPins.push(new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat),
color: "green"
}));
}
for (let i = 0; i < this.props.auctionHouses.length; i++) {
const auctionHouse = this.props.auctionHouses[i];
if (auctionHouse.latitude && auctionHouse.longitude && this.props.currentlySelectedAuctionId !== auctionHouse.auctionID) {
const pushPin = new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(auctionHouse.longitude, auctionHouse.latitude),
});
validPins.push(pushPin);
map.events.add("mouseover", pushPin, () => this.openPopup(auctionHouse, popup, map));
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave)
map.events.add("click", pushPin, () => this.props.onPinSelected(auctionHouse.auctionID));
}
}
map.markers.add(validPins);
}
openPopup(auctionHouse: IAuctionHouse, popup: atlas.Popup, map: atlas.Map) {
console.log("Mouseover on pin for auction house:" + auctionHouse.name, auctionHouse.city, auctionHouse.longitude, auctionHouse.latitude);
if (!map) {
console.log("Map is not initialized, cannot open popup.")
}
popup.setOptions({
content: "<div>" + auctionHouse.name + "<br>" + auctionHouse.city + "</div>",
position: [auctionHouse.longitude||0, auctionHouse.latitude||0],
})
popup.open(map);
}
render() {
return (
<div>
<div id="auction-house-location-map" />
<button
id="collateral-center-btn"
type="button"
className="btn btn-primary"
disabled={!this.isCollateralLocationFilled()}
onClick={this.centerMap}
>
Center on Collateral
</button>
</div>
);
}
}
export default AuctionAssignmentMap;
@@ -0,0 +1,14 @@
.auction-collateral-selection
{
margin-top: 0.5em;
}
.auction-collateral-selection
.card-body:not(.bg-warning) {
cursor: pointer;
}
.auction-collateral-selection
.bg-warning {
cursor: default;
}
@@ -0,0 +1,131 @@
import React from 'react';
import { shallow } from 'enzyme';
import AuctionCollateralSelection, { ICollateralSelection, IAuctionCollateralSelectionProps } from './AuctionCollateralSelection';
describe('Auction Collateral Selection Component', () => {
let auctionCollateralSelection: AuctionCollateralSelection;
let originalCollateralSelections: ICollateralSelection[];
let defaultProps: IAuctionCollateralSelectionProps;
let updatedProps:IAuctionCollateralSelectionProps;
beforeEach(() => {
originalCollateralSelections = [
{
collateral: {
collateralId: 1,
repoId: 1,
vin: '1234',
make: 'ford',
model: 'f150',
year: 1996,
repoCompanySelections: []
},
isSelected: false,
},
{
collateral: {
collateralId: 2,
repoId: 1,
vin: 'abcd',
make: 'dodge',
model: 'ram',
year: 2001,
auctionAssignmentId: 1,
repoCompanySelections: []
},
isSelected: false,
},
];
defaultProps = {
collateralSelections: originalCollateralSelections,
vehicleTypes: [],
collateralTypes: [],
bodyStyles: [],
collateralTypesToVehicleTypes: [],
onCollateralSelectionsChanged: (cs) => updatedProps.collateralSelections = cs,
onPrimaryCollateralChanged: (cid) => updatedProps.primaryCollateralId = cid,
};
updatedProps = Object.assign({}, defaultProps);
auctionCollateralSelection = new AuctionCollateralSelection(defaultProps);
});
it ('Renders AuctionCollateralSelectionRows', () => {
let wrapper = shallow(
<AuctionCollateralSelection
collateralSelections={originalCollateralSelections}
vehicleTypes={[]}
collateralTypes={[]}
bodyStyles={[]}
collateralTypesToVehicleTypes={[]}
onCollateralSelectionsChanged={() => {}}
onPrimaryCollateralChanged={() => {}}
/>
);
let selectionRow = wrapper.find(`[data-test='auction-collateral-selection-row']`);
expect(selectionRow.length).toBe(2);
});
it ('Updates Primary Collateral Id', () => {
auctionCollateralSelection.setPrimaryCollateral(originalCollateralSelections[0]);
expect(updatedProps.primaryCollateralId).toBe(1);
auctionCollateralSelection.setPrimaryCollateral(originalCollateralSelections[1]);
expect(updatedProps.primaryCollateralId).toBe(2);
});
it ('Selecting Collateral Copies and Calls onCollateralSelectionsChanged()', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.collateralSelections).not.toBeUndefined();
expect(updatedProps.collateralSelections).not.toBe(originalCollateralSelections);
expect(updatedProps.collateralSelections[0].isSelected).toBe(true);
});
it ('Selecting Collateral Without Primary Collateral Calls onPrimaryCollateralChanged()', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(1);
});
it ('Selecting Multiple Collaterals Does Not Continue Calling onPrimaryCollateralChanged()', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(1);
auctionCollateralSelection.toggleCollateralByIndex(1);
expect(updatedProps.primaryCollateralId).toBe(1);
});
it ('Unselecting All Collaterals Calls onPrimaryCollateralChanged() With Undefined', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(1);
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBeUndefined();
});
it ('Unselecting Primary Collateral With Another Selection Calls onPrimaryCollateralChanged() With Valid Id', () => {
updatedProps.collateralSelections[0].isSelected = true;
updatedProps.collateralSelections[1].isSelected = true;
updatedProps.primaryCollateralId = 1;
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(2);
});
it ('Selecting Collateral With Auction Triggers Nothing', () => {
let wasSelectionsUpdated = false;
let wasPrimaryCollateralUpdated = false;
updatedProps.collateralSelections[0].collateral.auctionAssignmentId = 1;
updatedProps.onCollateralSelectionsChanged = () => wasSelectionsUpdated = true;
updatedProps.onPrimaryCollateralChanged = () => wasPrimaryCollateralUpdated = true;
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(wasSelectionsUpdated).toBe(false);
expect(wasPrimaryCollateralUpdated).toBe(false);
});
});
@@ -0,0 +1,110 @@
import React from 'react';
import { IVehicleType, ICollateralType, IBodyStyle, ICollateralTypesToVehicleType } from "../../reducers/SettingsReducer";
import './AuctionCollateralSelection.css';
import AuctionCollateralSelectionRow from './auction-collateral-selection-row/AuctionCollateralSelectionRow';
import { ICollateral } from '../../services/service-types/RepoAccountTypes';
const cloneDeep = require('lodash.clonedeep');
export interface ICollateralSelection {
collateral: ICollateral;
isSelected: boolean;
}
export interface IAuctionCollateralSelectionProps {
collateralSelections: ICollateralSelection[];
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
primaryCollateralId?: number;
onCollateralSelectionsChanged: (collateralSelections: ICollateralSelection[]) => any;
onPrimaryCollateralChanged: (collateralId?: number) => any;
}
class AuctionCollateralSelection extends React.Component<IAuctionCollateralSelectionProps> {
constructor(props: IAuctionCollateralSelectionProps) {
super(props);
this.isCollateralSelectionPrimary = this.isCollateralSelectionPrimary.bind(this);
}
applyUpdate (index: number, updater: (x: ICollateralSelection) => void) {
const copiedCollaterSpecSelection = cloneDeep(this.props.collateralSelections) as ICollateralSelection[];
updater(copiedCollaterSpecSelection[index]);
this.props.onCollateralSelectionsChanged(copiedCollaterSpecSelection);
};
collateralUpdated(index: number, collateral: ICollateral) {
this.applyUpdate(index, (collateralSelection) => {
collateralSelection.collateral = collateral
});
};
toggleCollateralByIndex(index: number) {
const collateralSelection = this.props.collateralSelections[index];
if (this.isCollateralSelectable(collateralSelection)) {
if (!this.props.primaryCollateralId) {
// There is no primary collateral selected, default to this one
this.props.onPrimaryCollateralChanged(collateralSelection.collateral.collateralId);
} else if (collateralSelection.isSelected && this.isCollateralSelectionPrimary(collateralSelection)) {
// The selection with the primary collateral is about to be closed.
// Try and find a available collateral to be primary
this.props.onPrimaryCollateralChanged(this.getNextPrimaryCollateralId());
}
this.applyUpdate(index, (collateralSelection) => {
collateralSelection.isSelected = !collateralSelection.isSelected;
});
}
};
getNextPrimaryCollateralId() {
const nextCollateralSelection = this.props.collateralSelections.find(
(collateralSelection) => collateralSelection.isSelected && !this.isCollateralSelectionPrimary(collateralSelection)
);
if (nextCollateralSelection) {
return nextCollateralSelection.collateral.collateralId;
}
}
setPrimaryCollateral(collateralSelection: ICollateralSelection) {
this.props.onPrimaryCollateralChanged(
collateralSelection.collateral.collateralId,
)
}
isCollateralSelectable(collateralSelection: ICollateralSelection): boolean {
return !collateralSelection.collateral.auctionAssignmentId;
}
isCollateralSelectionPrimary(collateralSelection: ICollateralSelection) {
return this.props.primaryCollateralId === collateralSelection.collateral.collateralId;
}
render() {
return (
<div className="card auction-collateral-selection">
{this.props.collateralSelections.map((collateralSelection, index) =>
<AuctionCollateralSelectionRow
key={collateralSelection.collateral.collateralId}
collateralSelection={collateralSelection}
vehicleTypes={this.props.vehicleTypes}
collateralTypes={this.props.collateralTypes}
bodyStyles={this.props.bodyStyles}
collateralTypesToVehicleTypes={this.props.collateralTypesToVehicleTypes}
isCollateralPrimary={this.isCollateralSelectionPrimary(collateralSelection)}
onCollateralChange={(collateral) => this.collateralUpdated(index, collateral)}
onIsPrimaryClicked={() => this.setPrimaryCollateral(collateralSelection)}
onRowClicked={() => this.toggleCollateralByIndex(index)}
data-test="auction-collateral-selection-row"
/>
)}
</div>
)
}
};
export default AuctionCollateralSelection;
@@ -0,0 +1,88 @@
import React from 'react';
import { ShallowWrapper, shallow } from 'enzyme';
import AuctionCollateralSelectionRow from './AuctionCollateralSelectionRow';
import { ICollateralSelection } from '../AuctionCollateralSelection';
import moment from 'moment';
describe('AuctionCollateralSelectionRow Component', () => {
let wrapper: ShallowWrapper;
let collateralSelection: ICollateralSelection;
beforeEach(() => {
collateralSelection = {
isSelected: false,
collateral: {
collateralId: 2,
repoId: 1,
vin: 'abcd',
make: 'dodge',
model: 'ram',
year: 2001,
repoCompanySelections: []
},
};
wrapper = shallow(
<AuctionCollateralSelectionRow
collateralSelection={collateralSelection}
vehicleTypes={[]}
collateralTypes={[]}
bodyStyles={[]}
collateralTypesToVehicleTypes={[]}
isCollateralPrimary={false}
onCollateralChange={() => {}}
onIsPrimaryClicked={() => {}}
onRowClicked={() => {}}
/>
);
});
it ('Displays Collateral Correctly', () => {
const collateralDescription = wrapper.find(`[data-test='auction-collateral-selection-collateral-description']`);
expect(collateralDescription.length).toBe(1);
expect(collateralDescription.text()).toEqual('2001 dodge ram');
});
it ('Displays Collateral Selection Statuses Correctly', () => {
const collateralDescription = wrapper.find(`[data-test='auction-collateral-selection-collateral-description']`);
expect(collateralDescription.length).toBe(1);
expect(collateralDescription.text()).toEqual('2001 dodge ram');
let selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
expect(selectionStatus.length).toBe(1);
expect(selectionStatus.text().trim()).toEqual('(Not Selected)');
wrapper.setProps({
collateralSelection: {
...collateralSelection,
isSelected: true,
},
});
selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
expect(selectionStatus.length).toBe(1);
expect(selectionStatus.text().trim()).toEqual('(Selected)');
// wrapper.setProps({
// collateralSelection: {
// collateral: {
// ...collateralSelection.collateral,
// auctionAssignmentId: 1,
// },
// isSelected: false,
// },
// });
// selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
// expect(selectionStatus.length).toBe(1);
// expect(selectionStatus.text().trim()).toEqual('(Auction Already Created)');
});
it ('Calls Callback on Click', () => {
let wasCallTriggered = false;
wrapper.setProps({
onRowClicked: () => wasCallTriggered = true,
});
let rowHeader = wrapper.find(`[data-test='auction-collateral-selection-header']`);
rowHeader.simulate('click');
expect(wasCallTriggered).toBe(true);
});
});
@@ -0,0 +1,96 @@
import React from "react";
import { IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
import AuctionCollateralSpecs from "../auction-collateral-specs/AuctionCollateralSpecs";
import { ICollateralSelection } from "../AuctionCollateralSelection";
interface IAuctionCollateralSelectionRowProps {
collateralSelection: ICollateralSelection;
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
isCollateralPrimary: boolean;
onCollateralChange: (collateral: ICollateral) => any;
onIsPrimaryClicked: () => any;
onRowClicked: () => any;
}
const AuctionCollateralSelectionRow: React.FC<IAuctionCollateralSelectionRowProps> = (props) => {
const getCollateralDisplayName = () => {
const collateral = props.collateralSelection.collateral;
return `${collateral.year} ${collateral.make} ${collateral.model}`;
};
const getHeaderStatusText = () => {
if (props.collateralSelection.isSelected) {
return " (Selected)";
}
return " (Not Selected)";
};
const getHeaderBGClass = () => {
if (props.collateralSelection.isSelected) {
return "bg-success";
}
return "bg-light";
};
const getHeaderColorClass = () => {
if (props.collateralSelection.isSelected) {
return "text-white";
}
return "text-dark";
};
const getFontAwesomeIconClass = () => {
if (props.collateralSelection.isSelected) {
return "fa-check-square-o";
}
return "fa-square-o";
};
return (
<div className="auction-collateral-selection-row">
<div
className={"card-body " + getHeaderBGClass()}
onClick={props.onRowClicked}
data-test="auction-collateral-selection-header"
>
<h6 className={getHeaderColorClass()}>
<i className={"fa " + getFontAwesomeIconClass()} aria-hidden="true"/>
&nbsp;
<span data-test="auction-collateral-selection-collateral-description">
{getCollateralDisplayName()}
</span>
<i data-test="auction-collateral-selection-header-status-text">
{getHeaderStatusText()}
</i>
</h6>
</div>
{
props.collateralSelection.isSelected
?
<ul className="list-group list-group-flush" data-test="auction-collateral-selection-opened-section">
<li className="list-group-item">
<AuctionCollateralSpecs
collateral={props.collateralSelection.collateral}
vehicleTypes={props.vehicleTypes}
collateralTypes={props.collateralTypes}
bodyStyles={props.bodyStyles}
collateralTypesToVehicleTypes={props.collateralTypesToVehicleTypes}
onCollateralChange={props.onCollateralChange}
isCollateralPrimary={props.isCollateralPrimary}
onIsPrimaryClicked={props.onIsPrimaryClicked}
/>
</li>
</ul>
: null
}
</div>
);
};
export default AuctionCollateralSelectionRow;
@@ -0,0 +1,4 @@
.is-collateral-primary-check,
.is-collateral-primary-check * {
cursor: pointer;
}
@@ -0,0 +1,217 @@
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
import AuctionCollateralSpecs from "./AuctionCollateralSpecs";
describe("Auction Collateral Specs Component", () => {
let wrapper: ShallowWrapper;
let collateralTypes: ICollateralType[];
let collateralTypeToVehicleTypes: ICollateralTypesToVehicleType[];
let bodyStyles: IBodyStyle[];
let vehicleTypes: IVehicleType[];
let originalCollateral: ICollateral;
let updatedCollateral: ICollateral | undefined;
const collateralUpdated = (newCollateral: ICollateral) => {
updatedCollateral = newCollateral;
};
beforeEach(() => {
updatedCollateral = undefined;
originalCollateral = {
collateralId: 1,
repoId: 1,
vin: "1234",
make: "ford",
model: "f150",
year: 1996,
repoCompanySelections: []
};
collateralTypes = [
{
id: 1,
description: "Collateral Type 1",
lookupHandler: "",
},
{
id: 2,
description: "Collateral Type 2",
lookupHandler: "",
},
{
id: 3,
description: "Collateral Type 3",
lookupHandler: "",
},
];
collateralTypeToVehicleTypes = [
{
id: 1,
vehicleTypeId: 1,
collateralTypeId: 1,
},
{
id: 2,
vehicleTypeId: 1,
collateralTypeId: 2,
},
{
id: 3,
vehicleTypeId: 2,
collateralTypeId: 3,
},
];
bodyStyles = [
{
code: "body1",
description: "Body Style #1",
},
{
code: "body2",
description: "Body Style #2",
},
];
vehicleTypes = [
{
code: "1",
description: "Vehicle Type #1",
},
{
code: "2",
description: "Vehicle Type #2",
},
{
code: "3",
description: "Vehicle Type #3",
},
];
wrapper = shallow(
<AuctionCollateralSpecs
collateral={originalCollateral}
vehicleTypes={vehicleTypes}
collateralTypes={collateralTypes}
bodyStyles={bodyStyles}
collateralTypesToVehicleTypes={collateralTypeToVehicleTypes}
onCollateralChange={collateralUpdated}
isCollateralPrimary={false}
onIsPrimaryClicked={() => {}}
/>
);
});
it("Renders & Updates Vehicle Type", () => {
// Inital value is present
let vehicleTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-vehicle-type']`);
expect(vehicleTypeSelect.length).toBe(1);
expect(vehicleTypeSelect.props().value).toBe(undefined);
// Change triggers callback
vehicleTypeSelect.simulate("change", "1");
expect(updatedCollateral).not.toBeUndefined();
expect(updatedCollateral!.vehicleType).toEqual(parseInt(vehicleTypes[0].code));
// Change does not alter original prop object
expect(originalCollateral).not.toBe(updatedCollateral);
// Prop changes update input
wrapper = wrapper.setProps({
collateral: {
vehicleType: 2,
},
});
vehicleTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-vehicle-type']`);
expect(vehicleTypeSelect.props().value).toBe(2);
});
it("Renders & Updates Body Style", () => {
// Inital value is present
let bodyStyleSelect = wrapper.find(`[data-test='auction-collateral-specs-body-style']`);
expect(bodyStyleSelect.length).toBe(1);
expect(bodyStyleSelect.props().value).toBe(undefined);
// Change triggers callback
bodyStyleSelect.simulate("change", "body1");
expect(updatedCollateral).not.toBeUndefined();
expect(updatedCollateral!.bodyStyle).toEqual(bodyStyles[0].code);
// Change does not alter original prop object
expect(originalCollateral).not.toBe(updatedCollateral);
// Prop changes update input
wrapper = wrapper.setProps({
collateral: {
bodyStyle: "body2",
},
});
bodyStyleSelect = wrapper.find(`[data-test='auction-collateral-specs-body-style']`);
expect(bodyStyleSelect.props().value).toBe("body2");
});
it("Renders & Updates Collateral Style", () => {
// Inital value is present
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
// Change triggers callback
collateralTypeSelect.simulate("change", "3");
expect(updatedCollateral).not.toBeUndefined();
expect(updatedCollateral!.collateralTypeId).toEqual(collateralTypes[2].id);
// Change does not alter original prop object
expect(originalCollateral).not.toBe(updatedCollateral);
// Prop changes update input
wrapper = wrapper.setProps({
collateral: {
collateralTypeId: 2
},
});
collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.props().value).toBe(2);
});
it("Disabled Collateral Type Select when Vehicle Type is Undefined", () => {
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
expect(collateralTypeSelect.props().disabled).toBe(true);
});
it("No Collateral Type Options when Vehicle Type Does Not Match Collateral Types", () => {
wrapper = wrapper.setProps({
collateral: {
vehicleType: 3,
},
});
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
expect(collateralTypeSelect.props().disabled).toBe(false);
});
it("Valid Collateral Types Options when Vehicle Type Matches Collateral Types", () => {
wrapper = wrapper.setProps({
collateral: {
vehicleType: 1,
},
});
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
expect(collateralTypeSelect.props().disabled).toBe(false);
});
it ("Calls onIsPrimaryClicked() When Checkbox is Clicked", () => {
let wasCallTriggered = false;
wrapper.setProps({
onIsPrimaryClicked: () => wasCallTriggered = true,
});
let isPrimaryContainer = wrapper.find(`[data-test='auction-collateral-specs-is-primary-container']`);
isPrimaryContainer.simulate("click");
expect(wasCallTriggered).toBe(true);
});
});
@@ -0,0 +1,146 @@
import React from "react";
import { getCollateralTypesFromVehicleType, IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
import FormInput, { FormInputType } from "../../form-input/FormInput";
import "./AuctionCollateralSpecs.css";
const cloneDeep = require("lodash.clonedeep");
export interface IAuctionCollateralSpecsProps {
collateral: ICollateral;
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
isCollateralPrimary: boolean;
onCollateralChange: (collateral: ICollateral) => any;
onIsPrimaryClicked: () => any;
}
type ICollateralUpdater = (collateral: ICollateral) => void;
const AuctionCollateralSpecs: React.FC<IAuctionCollateralSpecsProps> = (props) => {
const applyChange = function( updater: ICollateralUpdater) {
const copiedCollateral = cloneDeep(props.collateral) as ICollateral;
updater(copiedCollateral);
props.onCollateralChange(copiedCollateral);
};
const handlePrimaryCollateralClicked = () => {
props.onIsPrimaryClicked();
};
const getCollateralDescription = () => {
return `${props.collateral.year} ${props.collateral.make} ${props.collateral.model} ${props.collateral.vin}`;
};
const getCollateralTypes = (vehicleTypeCode?: number) => {
return getCollateralTypesFromVehicleType(
props.collateralTypesToVehicleTypes,
props.collateralTypes,
vehicleTypeCode
);
};
const handleVehicleTypeChange = (selection: string) => {
applyChange((collateral) => {
collateral.vehicleType = selection ? parseInt(selection) : undefined;
const matchedCollateralTypes = getCollateralTypes(collateral.vehicleType);
if (matchedCollateralTypes.length === 1) {
collateral.collateralTypeId = matchedCollateralTypes[0].id;
} else {
collateral.collateralTypeId = undefined;
}
});
};
const isVehicleTypeSelected = () => {
return props.collateral.vehicleType !== undefined && props.collateral.vehicleType !== null;
};
const currentlyMatchedCollateralTypes = getCollateralTypesFromVehicleType(
props.collateralTypesToVehicleTypes,
props.collateralTypes,
props.collateral.vehicleType
);
return (
<div className="container">
<div className="row">
<div className="col-sm">
<div
className="custom-control custom-checkbox is-collateral-primary-check"
onClick={handlePrimaryCollateralClicked}
data-test="auction-collateral-specs-is-primary-container"
>
<input
type="checkbox"
className="custom-control-input"
checked={props.isCollateralPrimary}
readOnly={true}
/>
<label className="custom-control-label">Is Primary</label>
</div>
</div>
<div className="col-sm">
{getCollateralDescription()}
</div>
<div className="col-sm">
<FormInput
label="Vehicle Type"
required={true}
type={FormInputType.Selection}
value={props.collateral.vehicleType}
onChange={(vehicleType) => handleVehicleTypeChange(vehicleType)}
placeholder="Select Vehicle Type"
options={props.vehicleTypes.map((vehicleType) => {
return {
value: vehicleType.code,
display: vehicleType.description
};
})}
data-test="auction-collateral-specs-vehicle-type"
/>
</div>
<div className="col-sm">
<FormInput
label="Body Style"
required={true}
type={FormInputType.Selection}
value={props.collateral.bodyStyle}
onChange={(bodyStyle) => applyChange((collateral) => collateral.bodyStyle = bodyStyle)}
placeholder="Select Body Style"
options={props.bodyStyles.map((bodyStyle) => {
return {
value: bodyStyle.code,
display: bodyStyle.description
};
})}
data-test="auction-collateral-specs-body-style"
/>
</div>
<div className="col-sm">
<FormInput
label="Collateral Type"
required={true}
type={FormInputType.Selection}
value={props.collateral.collateralTypeId}
onChange={(collateralType) => applyChange((collateral) => collateral.collateralTypeId = parseInt(collateralType))}
placeholder="Select Collateral Type"
disabled={!isVehicleTypeSelected()}
options={currentlyMatchedCollateralTypes.map((collateralType) => {
return {
value: collateralType.id,
display: collateralType.description
};
})}
data-test="auction-collateral-specs-collateral-type"
/>
</div>
</div>
</div>
);
};
export default AuctionCollateralSpecs;
@@ -0,0 +1,16 @@
.card-view-main {
margin-top: 1em;
margin-bottom: 1em;
}
.card-view-main .card-body {
padding: .25em;
}
.card-view-main h6 {
margin: .5em;
}
.card-view-main .list-group-item {
padding: .25em 1em;
}
@@ -0,0 +1,46 @@
import React from 'react';
import { shallow } from "enzyme";
import CardView from './CardView';
describe('CardView Component', () => {
it('CardView Displays No Header', () => {
const cardView = shallow(<CardView rows={[]} />);
const header = cardView.find(`[data-test='card-view-header']`);
expect(header.length).toBe(0);
});
it('CardView Displays No Rows', () => {
const cardView = shallow(<CardView rows={[]} />);
const rows = cardView.find(`[data-test='card-view-row-parent']`);
expect(rows.length).toBe(0);
});
it('CardView Displays Header', () => {
const cardView = shallow(<CardView header="Header Test" rows={[]} />);
const header = cardView.find(`[data-test='card-view-header']`);
expect(header.length).toBe(1);
expect(header.text()).toEqual('Header Test');
});
it('CardView Displays Valid Text Rows', () => {
const cardView = shallow(<CardView rows={[
'Row 1',
'Row 2',
]} />);
const rows = cardView.find(`[data-test='card-view-row-parent']`);
expect(rows.length).toBe(2);
expect(rows.at(0).text()).toEqual('Row 1');
expect(rows.at(1).text()).toEqual('Row 2');
});
it('CardView Displays Valid HTML Rows', () => {
const cardView = shallow(<CardView rows={[
<p data-test='card-view-test-html-child'>Child Text</p>,
]} />);
const pChild = cardView.find(`[data-test='card-view-test-html-child']`);
expect(pChild.length).toBe(1);
expect(pChild.text()).toEqual('Child Text');
});
});
@@ -0,0 +1,39 @@
import React from 'react';
import './CardView.css';
interface ICardViewProps {
header?: any;
rows: any[];
}
const CardView: React.FC<ICardViewProps> = (props) => {
const tryDisplayHeader = () => {
if (props.header) {
return (
<div className="card-body bg-secondary">
<h6 className="text-white" data-test="card-view-header">{props.header}</h6>
</div>
)
}
}
return (
<div className="card card-view-main">
{tryDisplayHeader()}
<ul className="list-group list-group-flush">
{
props.rows.map((item, index) => {
return (
<li key={index} className="list-group-item" data-test="card-view-row-parent">
{item}
</li>
)
})
}
</ul>
</div>
)
}
export default CardView;
@@ -0,0 +1,3 @@
.collateral-view-row p {
margin-bottom: 0;
}
@@ -0,0 +1,116 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import CollateralDisplay, { ICollateralDisplay } from './CollateralDisplay';
import moment from 'moment';
import { ICollateral } from '../../services/service-types/RepoAccountTypes';
describe('CollateralDisplay Component', () => {
let collateralDisplay: ShallowWrapper;
let retitleClickValue = 0;
let refreshCollateralValue = false;
const retitledClicked = (collateralId: number) => {
retitleClickValue = collateralId;
};
const refreshCollateralClicked = () => {
refreshCollateralValue = true;
}
let dummyCollateral1: ICollateralDisplay = {
collateral: {
vin: '1234abcd',
make: 'ford',
model: 'fiesta',
year: 1865,
collateralId: 1970,
} as ICollateral,
refreshCollateral: refreshCollateralClicked,
}
let dummyCollateral2: ICollateralDisplay = {
collateral: {
vin: 'abcd1234',
make: 'ferrari',
model: 'enzo',
year: 1916,
collateralId: 1968,
refCollateralId: '1998',
retitledInLandmarkName: moment('1914-06-28'),
} as ICollateral,
refreshCollateral: () => {},
}
beforeEach(() => {
collateralDisplay = shallow(
<CollateralDisplay
isAccountClosed={false}
collateral={[dummyCollateral1, dummyCollateral2]}
/>
)
});
afterEach(() => {
retitleClickValue = 0;
refreshCollateralValue = false;
})
const getChildCardView = () => {
return collateralDisplay.find('CardView').dive();
}
it('CollateralDisplay Renders Year', () => {
const collateralYear = getChildCardView().find(`[data-test='collateral-display-year']`);
expect(collateralYear.length).toBe(2);
expect(collateralYear.at(0).text()).toEqual('1865');
expect(collateralYear.at(1).text()).toEqual('1916');
});
it('CollateralDisplay Renders Make', () => {
const collateralMake = getChildCardView().find(`[data-test='collateral-display-make']`);
expect(collateralMake.length).toBe(2);
expect(collateralMake.at(0).text()).toEqual('ford');
expect(collateralMake.at(1).text()).toEqual('ferrari');
});
it('CollateralDisplay Renders Model', () => {
const collateralModel = getChildCardView().find(`[data-test='collateral-display-model']`);
expect(collateralModel.length).toBe(2);
expect(collateralModel.at(0).text()).toEqual('fiesta');
expect(collateralModel.at(1).text()).toEqual('enzo');
});
it('CollateralDisplay Renders Vin', () => {
const collateralVin = getChildCardView().find(`[data-test='collateral-display-vin']`);
expect(collateralVin.length).toBe(2);
expect(collateralVin.at(0).text()).toEqual('1234abcd');
expect(collateralVin.at(1).text()).toEqual('abcd1234');
});
it('CollateralDisplay Collateral Refresh Button is Disabled', () => {
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
expect(collateralRefreshBtn.length).toBe(2);
expect(collateralRefreshBtn.at(0).props().disabled).toEqual(true);
});
it('CollateralDisplay Collateral Refresh Button is Enabled', () => {
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
expect(collateralRefreshBtn.length).toBe(2);
expect(collateralRefreshBtn.at(1).props().disabled).toEqual(false);
});
it('CollateralDisplay Calls Collateral Refresh Callback', () => {
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
expect(collateralRefreshBtn.length).toBe(2);
collateralRefreshBtn.at(0).simulate('click');
expect(refreshCollateralValue).toEqual(true);
});
it('CollateralDisplay Displays Retitled Date', () => {
const collateralRetitleDate = getChildCardView().find(`[data-test='collateral-display-retitle-date']`);
expect(collateralRetitleDate.length).toBe(2);
expect(collateralRetitleDate.last().text()).toBe('1914-06-28');
expect(collateralRetitleDate.first().text()).toBe('');
});
});
@@ -0,0 +1,59 @@
import React from "react";
import { ICollateral } from "../../services/service-types/RepoAccountTypes";
import CardView from "../card-view/CardView";
import DateFormatting from "./../../shared/formatting/DateFormatting";
import "./CollateralDisplay.css";
export interface ICollateralDisplay {
collateral: ICollateral;
refreshCollateral: () => any;
}
export interface ICollateralDisplayProps {
isAccountClosed: boolean;
collateral: ICollateralDisplay[];
}
const CollateralDisplay: React.FC<ICollateralDisplayProps> = (props: ICollateralDisplayProps) => {
return (
<CardView
rows={props.collateral.map((collateralDisplay) => (
<div key={collateralDisplay.collateral.collateralId} className="row collateral-view-row">
<div className="col-sm">
<p>Year:</p>
<p><b data-test="collateral-display-year">{collateralDisplay.collateral.year}</b></p>
</div>
<div className="col-sm">
<p>Make:</p>
<p><b data-test="collateral-display-make">{collateralDisplay.collateral.make}</b></p>
</div>
<div className="col-sm">
<p>Model:</p>
<p><b data-test="collateral-display-model">{collateralDisplay.collateral.model}</b></p>
</div>
<div className="col-sm">
<p>VIN:</p>
<p><b data-test="collateral-display-vin">{collateralDisplay.collateral.vin}</b></p>
</div>
<div className="col-sm">
<p>Retitled Date:</p>
<p><b data-test="collateral-display-retitle-date">{DateFormatting.FormatYYYYMMDD(collateralDisplay.collateral.retitledInLandmarkName)}</b></p>
</div>
<div className="col-sm text-center my-auto">
<button
className={"btn " + (props.isAccountClosed ? "btn-secondary" : "btn-info")}
onClick={() => collateralDisplay.refreshCollateral()}
disabled={!collateralDisplay.collateral.refCollateralId || props.isAccountClosed}
data-test="collateral-display-refresh-btn"
>
Collateral Refresh
</button>
</div>
</div>
))}
/>
);
};
export default CollateralDisplay;
@@ -0,0 +1,3 @@
.vin-disp {
font-size: smaller;
}
@@ -0,0 +1,31 @@
import React from 'react';
import {shallow, ShallowWrapper} from 'enzyme';
import CollateralTextSummary, {ICollateralSummaryInfo} from "./CollateralTextSummary";
describe('CollateralTextSummary Component', () => {
let collateralDisplay: ShallowWrapper;
let dummyCollateral1: ICollateralSummaryInfo = {
vin: '1234abcd',
make: 'ford',
model: 'FIESTA',
year: 1865,
collateralId: 1970
};
beforeEach(() => {
collateralDisplay = shallow(
<CollateralTextSummary collateral={dummyCollateral1}/>
)
});
const getView = () => {
return collateralDisplay.find('span');
};
it('CollateralTextSummary Renders ', () => {
const collateral = getView();
expect(collateral.first().text()).toEqual('1865 Ford FIESTA 1234abcd');
});
});
@@ -0,0 +1,23 @@
import React from 'react';
import "./CollateralTextSummary.css";
import StringFormatting from "../../../shared/formatting/StringFormatting";
export interface ICollateralSummaryInfo {
vin: string;
make: string;
model: string;
year: number;
collateralId: number;
}
export interface ICollateralTextSummaryProps {
collateral: ICollateralSummaryInfo;
}
const CollateralTextSummary: React.FC<ICollateralTextSummaryProps> = (props: ICollateralTextSummaryProps) => {
return (
<span>{props.collateral.year} {StringFormatting.ToTitleCase(props.collateral.make)} {props.collateral.model} <span className='vin-disp'>{props.collateral.vin}</span></span>
);
};
export default CollateralTextSummary;
@@ -0,0 +1,69 @@
import React from 'react';
import {shallow} from "enzyme";
import {_AuctionDisplay} from "./AuctionDisplay";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAuctionHouse} from "../../reducers/AuctionHouseReducer";
import AsyncState from "../../shared/enumerations/AsyncState";
describe('AuctionDisplay Component', () => {
it('Auction looks up company by id', () => {
const loader = () => {};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [],
state: AsyncState.Success
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(aucDisplay.length).toBe(1);
expect(aucDisplay.text()).toBe("mmaa");
});
it('Load if status is not success', () => {
const loader = () => {};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [],
state: AsyncState.Loading
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(aucDisplay.length).toBe(1);
expect(aucDisplay.text()).toBe("Loading...")
});
it('Loads Auction Houses When Inactive', () => {
let didLoad = false;
const loader = () => {didLoad = !didLoad};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [],
state: AsyncState.Inactive
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(didLoad).toBe(true);
});
it("Display Auction House Name when Auction House Name Found", () => {
const loader = () => {};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [{auctionID: "mmaa",
name: "name",
active: "yes",
address_Line_1: "11",
address_Line_2: "11",
state: "WI",
city: "Eagle Creek",
phone: "13131",
fax: "123213",
zip: 55555,
country: "USA" }
],
state: AsyncState.Success
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(aucDisplay.text()).toBe("name");
});
});
@@ -0,0 +1,60 @@
import React from "react";
import {connect} from "react-redux";
import {getAllAuctionHouses} from "../../actions/AuctionHouseActions";
import {IAuctionHouse} from "../../reducers/AuctionHouseReducer";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../store/Store";
interface IAuctionDisplayProps {
auctionId: string;
}
interface IPropsFromState {
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
auctionHouses: state.auctionHouseState.auctionHouses,
};
};
interface IPropsFromDispatch {
loadAuctionHouses: () => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
loadAuctionHouses: getAllAuctionHouses,
};
type Props = IAuctionDisplayProps & IPropsFromState & IPropsFromDispatch;
const AuctionDisplay: React.FC<Props> = (props) => {
const auctionLookup = (auctionId: string): string => {
// Did not put this in useEffect because it has an odd timing issue and they ALL load.
if (props.auctionHouses.state === AsyncState.Inactive) {
props.loadAuctionHouses();
}
if (props.auctionHouses && props.auctionHouses.state === AsyncState.Success) {;
const auctionHouseSet = props.auctionHouses.data as IAuctionHouse[];
const auctionHouse = auctionHouseSet.find((house: IAuctionHouse) => {
return house.auctionID.toLowerCase() === props.auctionId.toLowerCase();
});
return auctionHouse ? auctionHouse.name : props.auctionId;
} else {
return "Loading..."; // @TODO switch to showing notification.
}
};
return (
<span data-test={props.auctionId + "-auc-display"}>{auctionLookup(props.auctionId)}</span>
);
};
export const _AuctionDisplay = AuctionDisplay;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AuctionDisplay);
@@ -0,0 +1,67 @@
import React from 'react';
import {shallow} from "enzyme";
import {_RepoCompanyDisplay} from "./RepoCompanyDisplay";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import AsyncState from "../../shared/enumerations/AsyncState";
import {IRepoCompany} from "../../reducers/RepoCompanyReducer";
const findByTestAttribute = (component: ShallowWrapper, attr: string) => {
const wrapper = component.find(`[data-test='${attr}']`);
return wrapper;
};
describe('Repo Company Display Component', () => {
it('looks up company by id', () => {
const loader = () => {};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [],
state: AsyncState.Success
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(aucDisplay.length).toBe(1);
});
it('displays loading if status is not success', () => {
const loader = () => {};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [],
state: AsyncState.Loading
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(aucDisplay.length).toBe(1);
expect(aucDisplay.text()).toBe("Loading...")
});
it('Calls Load repo Companies when inactive',() => {
var didLoad = false;
const loader = () => {didLoad = !didLoad};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [],
state: AsyncState.Inactive
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(didLoad).toBe(true);
});
it('Displays Repo Company Name when Repo Company Name Is Found', () => {
const loader = () => {};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [
{id: 23,
name: "company"
}
],
state: AsyncState.Success
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(aucDisplay.text()).toBe("company")
});
});
@@ -0,0 +1,60 @@
import React from 'react';
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../store/Store";
import AsyncState from "../../shared/enumerations/AsyncState";
import {connect} from "react-redux";
import {IRepoCompany} from "../../reducers/RepoCompanyReducer";
import {getAllRepoCompanies} from "../../actions/RepoCompanyActions";
interface IRepoCompanyDisplayProps {
repoCompanyId: number;
}
interface IPropsFromState {
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoCompanies: state.repoCompanyState.repoCompanies,
};
};
interface IPropsFromDispatch {
loadRepoCompanies: () => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
loadRepoCompanies: getAllRepoCompanies,
};
type Props = IRepoCompanyDisplayProps & IPropsFromState & IPropsFromDispatch;
const RepoCompanyDisplay: React.FC<Props> = (props) => {
const RepoCompanyLookup = (repoCompanyId: number): string => {
// Did not put this in useEffect because it has an odd timing issue and they ALL load.
if (props.repoCompanies.state === AsyncState.Inactive) {
props.loadRepoCompanies();
}
if (props.repoCompanies && props.repoCompanies.state === AsyncState.Success) {
const repoCompanieSet = props.repoCompanies.data as IRepoCompany[];
const repoCompany = repoCompanieSet.find((repo: IRepoCompany) => {
return repo.id === repoCompanyId;
});
return repoCompany ? repoCompany.name : (props.repoCompanyId + '');
} else {
return "Loading...";
}
};
return (
<span data-test={props.repoCompanyId + '-rc-display'}>{RepoCompanyLookup(props.repoCompanyId)}</span>
)
};
export const _RepoCompanyDisplay = RepoCompanyDisplay;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(RepoCompanyDisplay);
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom";
import Footer from "./Footer";
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<Footer />, div);
ReactDOM.unmountComponentAtNode(div);
});
@@ -0,0 +1,30 @@
import "./Footer.css";
import {BUILD_STAMP, VERSION} from "../../environments/version";
import React from "react";
import watermark from "../../assets/light-house-corner-watermark.png";
const Footer: React.FC = () => {
const versionHint = VERSION.version + "-" + BUILD_STAMP;
const currentYear = new Date().getFullYear();
return (
<div>
<div className="watermark-outer">
<div className="watermark-inner">
<img src={watermark} className="watermark-primary" alt="" />
</div>
</div>
<footer className="py-3 bg-dark">
<div className="container">
<p
className="m-0 text-center text-white"
title={versionHint}>Copyright &copy; {currentYear} Landmark Credit Union
</p>
</div>
</footer>
</div>
);
};
export default Footer;
@@ -0,0 +1,15 @@
.form-input-label {
margin-bottom: 0;
font-size: 0.95em;
color: #555;
text-align: left;
}
.small-checkbox{
width: 1.5em;
height: 1.5em;
margin:0px;
padding:0px;
align-self:center;
margin-top: .25em;
}
@@ -0,0 +1,98 @@
import { shallow, mount } from "enzyme";
import React from "react";
import FormInput, { FormInputType } from "./FormInput";
describe("FormInput Component", () => {
it ("Renders Label", () => {
const wrapper = shallow(
<FormInput
label="The Label"
type={FormInputType.Selection}
value={1}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
const selectionInput = wrapper.find(`[data-test='label-text']`);
expect(selectionInput.length).toBe(1);
expect(wrapper.find(`[data-test='error-message']`).length).toBe(0);
});
it ("Passes Proper Error State To FormInputselector", () => {
const wrapper = mount(
<FormInput
label="The Label"
type={FormInputType.Selection}
value={1}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
expect(wrapper.state("isInputInErrorState")).toBe(false);
});
it ("Loads Required asterisk", () => {
const wrapper = mount(
<FormInput
required = {true}
label="The Label"
type={FormInputType.Selection}
value={undefined}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
expect(wrapper.find(`[data-test='required-asterisk']`).length).toBe(1);
expect(wrapper.state("isInputInErrorState")).toBe(false);
});
it ("Ignores labels if there is no label", () => {
const wrapper = mount(
<FormInput
required = {true}
type={FormInputType.Selection}
value={undefined}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
expect(wrapper.find(`[data-test='form-input-label']`).length).toBe(0);
expect(wrapper.state("isInputInErrorState")).toBe(false);
});
it ("Turns red when hasError is set to true", () => {
const mountedWrapper = mount(
<FormInput
required={true}
type={FormInputType.Selection}
value={"undefined"}
options={[]}
onChange={() => {}}
placeholder="Option"
hasError={true}
/>
);
mountedWrapper.setProps(
{
value: "TEst"
}
);
mountedWrapper.update();
expect(mountedWrapper.state("isInputInErrorState")).toBe(true);
});
});
@@ -0,0 +1,128 @@
import { Moment } from "moment";
import React from "react";
import FormInputSelector from "./form-input-selector/FormInputSelector";
import "./FormInput.css";
export enum FormInputType {
Selection,
Datetime,
Text,
Textarea,
Number,
Checkbox
}
export enum FormInputNumericalType {
Monetary,
Percentage
}
export interface IFormInputSelectOption {
value: string | number;
display: string | number;
isDisabled?: boolean;
}
interface IFormInputWrapper<T1, T2> {
type: FormInputType;
value: T1;
label?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
onChange: (newValue: T2) => any;
hasError?: boolean;
}
export interface FormInputText extends IFormInputWrapper<string | number | undefined, string> {
type: FormInputType.Text;
}
export interface FormInputTextArea extends IFormInputWrapper<string | number | undefined, string> {
type: FormInputType.Textarea;
}
export interface FormInputDate extends IFormInputWrapper<Moment | undefined, Moment | undefined> {
type: FormInputType.Datetime;
minDate?: Moment;
maxDate?: Moment;
}
export interface FormInputNumber extends IFormInputWrapper<number | undefined, number | undefined> {
type: FormInputType.Number;
precision?: number;
displayType?: FormInputNumericalType;
}
export interface FormInputSelection extends IFormInputWrapper<string | number | undefined, string> {
type: FormInputType.Selection;
options: IFormInputSelectOption[];
}
export interface FormInputCheckbox extends IFormInputWrapper<string | undefined, boolean> {
type: FormInputType.Checkbox;
checked?: boolean;
}
export type FormInputSpecs =
FormInputText |
FormInputTextArea |
FormInputDate |
FormInputSelection |
FormInputNumber |
FormInputCheckbox;
interface IFormInputState {
isInputInErrorState: boolean;
}
class FormInput extends React.Component<FormInputSpecs, IFormInputState> {
constructor(props: FormInputSpecs) {
super(props);
this.state = { isInputInErrorState: false };
this.setInputValidity = this.setInputValidity.bind(this);
}
componentDidUpdate(prevProps: FormInputSpecs) {
if (prevProps.value !== this.props.value) {
this.setInputValidity();
}
if(prevProps.hasError !== this.props.hasError){
this.setInputValidity();
}
}
setInputValidity() {
this.setState({
isInputInErrorState:
this.props.hasError === true ||
(!!this.props.required &&
(
this.props.value === null ||
this.props.value === undefined ||
this.props.value === ""
))
});
}
render() {
return (
<div>
{ this.props.label ?
<p className={"form-input-label " + (this.state.isInputInErrorState ? "text-danger" : "")}>
<span data-test="label-text">{this.props.label}</span>
{ this.props.required ?
<span className="text-danger">&nbsp;<b data-test="required-asterisk">*</b></span>
: null}
</p>
: null}
<FormInputSelector inputSpecs={this.props} isError={this.state.isInputInErrorState} onBlur={this.setInputValidity} />
</div>
);
}
}
export default FormInput;
@@ -0,0 +1,8 @@
@import "node_modules/bootstrap/scss/functions";
@import "node_modules/bootstrap/scss/variables";
@import "node_modules/bootstrap/scss/mixins";
.form-input-is-error {
border-width: 1px;
border-color: lighten($danger, 15%);
}
@@ -0,0 +1,217 @@
import { shallow } from "enzyme";
import moment, { Moment } from "moment";
import React from "react";
import DateFormatting from "../../../shared/formatting/DateFormatting";
import { FormInputDate, FormInputSelection, FormInputText, FormInputTextArea, FormInputType, FormInputCheckbox, FormInputNumber } from "../FormInput";
import FormInputSelector from "./FormInputSelector";
describe("FormInputSelector Component", () => {
it ("Renders and Updates Selection Input", () => {
const inputSpecs: FormInputSelection = {
type: FormInputType.Selection,
value: 1,
options: [
{ value: 1, display: "Option #1" },
{ value: 2, display: "Option #2" },
{ value: 3, display: "Option #3" }
],
onChange: (value) => callbackValue = value,
placeholder: "Select an Option"
};
let callbackValue: string | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let selectionInput = wrapper.find(`[data-test='select-input']`);
expect(selectionInput.length).toBe(1);
const selectionPlaceholderOption = wrapper.find(`[data-test='select-input-placeholder-option']`);
expect(selectionPlaceholderOption.length).toBe(1);
expect(selectionPlaceholderOption.text()).toEqual("Select an Option");
const selectionInputOptions = wrapper.find(`[data-test='select-input-option']`);
expect(selectionInputOptions.length).toBe(3);
expect(selectionInputOptions.at(0).text()).toEqual("Option #1");
expect(selectionInputOptions.at(1).text()).toEqual("Option #2");
expect(selectionInputOptions.at(2).text()).toEqual("Option #3");
expect(callbackValue).toBeUndefined();
selectionInput.simulate("change", { target: { value: "2" }});
expect(callbackValue).toEqual("2");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: 3
},
});
selectionInput = wrapper.find(`[data-test='select-input']`);
expect(selectionInput.props().value).toBe(3);
});
it ("Renders and Updates Text Input", () => {
const inputSpecs: FormInputText = {
type: FormInputType.Text,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: string | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let textInput = wrapper.find(`[data-test='text-input']`);
expect(textInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
textInput.simulate("change", { target: { value: "Different Text" }});
expect(callbackValue).toEqual("Different Text");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: "Even Newer Text"
},
});
textInput = wrapper.find(`[data-test='text-input']`);
expect(textInput.props().value).toBe("Even Newer Text");
});
it ("Renders and Updates Text Area Input", () => {
const inputSpecs: FormInputTextArea = {
type: FormInputType.Textarea,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: string | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let textAreaInput = wrapper.find(`[data-test='textarea-input']`);
expect(textAreaInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
textAreaInput.simulate("change", { target: { value: "Different Text" }});
expect(callbackValue).toEqual("Different Text");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: "Even Newer Text"
},
});
textAreaInput = wrapper.find(`[data-test='textarea-input']`);
expect(textAreaInput.props().value).toBe("Even Newer Text");
});
it ("Renders and Updates Date Input", () => {
const inputSpecs: FormInputDate = {
type: FormInputType.Datetime,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: Moment | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let dateInput = wrapper.find(`[data-test='date-input']`);
expect(dateInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
dateInput.simulate("change", new Date(2019, 1, 7));
expect(callbackValue).not.toBeUndefined();
expect(DateFormatting.FormatMMDDYYYY(callbackValue)).toEqual("02/07/2019");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: moment("2015-08-24")
},
});
dateInput = wrapper.find(`[data-test='date-input']`);
expect(dateInput.props().selected).toEqual(moment("2015-08-24").toDate());
});
it("Renders and Updates Checkbox Input", () => {
let callbackValue : boolean | undefined;
const inputSpecs : FormInputCheckbox = {
type: FormInputType.Checkbox,
value: undefined,
checked: false,
onChange: (value) => callbackValue = value
}
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let checkboxinput = wrapper.find(`[data-test='checkbox-input']`);
checkboxinput.simulate("change", {target : {checked: true}});
expect(callbackValue).toBe(true);
wrapper.setProps({
inputSpecs: {
...inputSpecs,
checked: true
},
});
checkboxinput = wrapper.find(`[data-test='checkbox-input']`);
expect(checkboxinput.props().value).toBe("true");
expect(checkboxinput.props().checked).toBe(true)
});
it ("Renders and Updates Numerical Input", () => {
const inputSpecs: FormInputNumber = {
type: FormInputType.Number,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: number | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let textInput = wrapper.find(`[data-test='number-input']`);
expect(textInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
textInput.simulate("change", { target: { value: 5 }});
expect(callbackValue).toEqual(5);
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: 6
},
});
textInput = wrapper.find(`[data-test='number-input']`);
expect(textInput.props().value).toBe(6);
});
});
@@ -0,0 +1,184 @@
import { Moment } from "moment";
import React from "react";
import DatePicker from "react-datepicker";
import DateConversions from "../../../shared/conversions/DateConversions";
import NumericalFormatting from "../../../shared/formatting/NumericalFormatting";
import StringFormatting from "../../../shared/formatting/StringFormatting";
import { FormInputNumericalType, FormInputSpecs, FormInputType } from "../FormInput";
import "./FormInputSelector.scss";
interface IFormInputSelectorProps {
inputSpecs: FormInputSpecs;
isError: boolean;
onBlur?: () => any;
onFocus?: () => any;
}
const FormInputSelector: React.FC<IFormInputSelectorProps> = (props) => {
const handleBasicInputChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement>, callback: (data: string) => any) => {
callback(StringFormatting.TrimLeft(event.target.value));
};
const handleDateInputChange = (date: Date | null, callback: (data: Moment | undefined) => any) => {
const momentDate = DateConversions.dateToMoment(date);
callback(momentDate);
};
const numericalInputChange = (event: React.ChangeEvent<HTMLInputElement>, callback: (data: number | undefined) => any, precision?: number) => {
const value = event.target.value;
if (!value) {
callback(undefined);
return;
}
let floatValue = parseFloat(value);
if (precision) {
floatValue = NumericalFormatting.FormatPreciseFlooredNumber(floatValue, precision);
}
callback(floatValue);
};
const handleCheckboxInputChange = (event: React.ChangeEvent<HTMLInputElement>, callback: (data: boolean) => any) => {
callback(event.target.checked);
}
const getInputClassName = () => {
let classes = "form-control";
if (props.isError) {
classes += " form-input-is-error";
}
return classes;
};
const getAdjustedInputValue = (value: string | number | undefined) => {
if (value === null || value === undefined) {
return "";
}
return value;
};
switch(props.inputSpecs.type) {
case FormInputType.Selection:
const selectionOnChange = props.inputSpecs.onChange;
return (
<select
className={getInputClassName()}
value={getAdjustedInputValue(props.inputSpecs.value)}
onChange={(event) => handleBasicInputChange(event, selectionOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="select-input"
>
{ props.inputSpecs.placeholder ?
<option value="" disabled={true} data-test="select-input-placeholder-option">{props.inputSpecs.placeholder}</option>
: null}
{
props.inputSpecs.options.map((option, index) =>
<option
value={option.value}
key={index}
data-test="select-input-option"
>
{option.display}
</option>
)
}
</select>
);
case FormInputType.Textarea:
const textAreaOnChange = props.inputSpecs.onChange;
return (
<textarea
className={getInputClassName()}
placeholder={props.inputSpecs.placeholder}
value={props.inputSpecs.value}
onChange={(event) => handleBasicInputChange(event, textAreaOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="textarea-input"
/>
);
case FormInputType.Text:
const textOnChange = props.inputSpecs.onChange;
return (
<input
className={getInputClassName()}
placeholder={props.inputSpecs.placeholder}
value={props.inputSpecs.value}
onChange={(event) => handleBasicInputChange(event, textOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="text-input"
/>
);
case FormInputType.Datetime:
const datetimeOnChange = props.inputSpecs.onChange;
return (
<DatePicker
className={getInputClassName()}
placeholderText={props.inputSpecs.placeholder ? props.inputSpecs.placeholder : "mm/dd/yyyy"}
minDate={props.inputSpecs.minDate ? props.inputSpecs.minDate.toDate() : undefined}
selected={DateConversions.momentToDate(props.inputSpecs.value)}
disabled={props.inputSpecs.disabled}
onChange={(date) => handleDateInputChange(date, datetimeOnChange)}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="date-input"
/>
);
case FormInputType.Number:
const numberOnChange = props.inputSpecs.onChange;
const precision = props.inputSpecs.precision;
const step = precision ? Math.pow(10, - Math.floor(precision)).toString() : undefined;
return (
<div className="input-group">
{ props.inputSpecs.displayType === FormInputNumericalType.Monetary ?
<div className="input-group-prepend">
<span className="input-group-text">$</span>
</div>
: null}
<input
type="number"
step={step}
className={getInputClassName()}
placeholder={props.inputSpecs.placeholder}
value={props.inputSpecs.value}
onChange={(event) => numericalInputChange(event, numberOnChange, precision)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="number-input"
/>
{ props.inputSpecs.displayType === FormInputNumericalType.Percentage ?
<div className="input-group-append">
<span className="input-group-text">%</span>
</div>
: null}
</div>
);
case FormInputType.Checkbox:
const checkboxValueOnChange = props.inputSpecs.onChange;
return (
<input
value = {props.inputSpecs.checked? "true" : "false"}
className="pl-5 small-checkbox"
placeholder={props.inputSpecs.placeholder}
checked = {(props.inputSpecs.checked)}
onChange={(event) => handleCheckboxInputChange(event, checkboxValueOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
type = "checkbox"
data-test="checkbox-input"
/>
);
}
};
export default FormInputSelector;
@@ -0,0 +1,20 @@
.notification-bar {
.fa {
margin-right: 0.75em;
}
.notification-bar-button-text {
font-size: 1rem;
}
.notification-bar-sub-text-container {
margin-left: 1.75em;
hr {
margin: 2px;
width: 67%;
}
p {
font-size: 0.8rem;
margin: 0;
}
}
}
@@ -0,0 +1,74 @@
import React from 'react';
import { shallow } from 'enzyme';
import NotificationBar, { NotificationBarType } from './NotificationBar';
describe('NotificationBar', () => {
it ('Renders text', () => {
let messageText = "Text to display."
let wrapper = shallow(<NotificationBar text={messageText} type={NotificationBarType.Danger} />);
const component = wrapper.find(`[data-test='notification-bar']`)
expect(component.length).toBe(1);
expect(component.text()).toMatch(messageText);
});
it ('Renders Loading alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Loading} />);
const component = wrapper.find(`[data-test='loading-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Danger alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Danger} />);
const component = wrapper.find(`[data-test='danger-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Warning alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Warning} />);
const component = wrapper.find(`[data-test='warning-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Success alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Success} />);
const component = wrapper.find(`[data-test='success-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Info alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Info} />);
const component = wrapper.find(`[data-test='info-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Diabled alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Disabled} />);
const component = wrapper.find(`[data-test='exclamation-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Dismiss Button', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Success} isButtonPresent={true} />);
const component = wrapper.find(`[data-test='notification-bar-dismiss-button']`)
expect(component.length).toBe(1);
});
it ('Dismiss Button Triggers Callback', () => {
let valToUpdate = false;
let wrapper = shallow(
<NotificationBar
text=""
type={NotificationBarType.Success}
isButtonPresent={true}
onButtonClick={() => valToUpdate = true}
/>
);
expect(valToUpdate).toBe(false);
const component = wrapper.find(`[data-test='notification-bar-dismiss-button']`)
expect(component.length).toBe(1);
component.simulate('click');
expect(valToUpdate).toBe(true);
});
});
@@ -0,0 +1,96 @@
import React from 'react';
import './NotificationBar.scss';
export enum NotificationBarType {
Success = 'success',
Warning = 'warning',
Danger = 'danger',
Loading = 'primary',
Info = 'info',
Disabled = 'secondary'
}
interface INotificationBarProps {
type: NotificationBarType;
text: string;
subText?: string;
isButtonPresent?: boolean;
buttonText?: string;
fontAwesomeIcon?: string;
onButtonClick?: () => any;
}
const getNotificationIcon = (alertType: NotificationBarType, fontAwesomeIcon?: string) => {
if (fontAwesomeIcon) {
return <i className={'fa ' + fontAwesomeIcon} data-test="misc-icon"></i>
}
switch (alertType) {
case NotificationBarType.Loading:
return (
<i className="fa fa-spinner fa-spin" data-test="loading-icon"></i>
);
case NotificationBarType.Danger:
return (
<i className="fa fa-times-circle" data-test="danger-icon"></i>
);
case NotificationBarType.Warning:
return (
<i className="fa fa-exclamation-triangle" data-test="warning-icon"></i>
);
case NotificationBarType.Success:
return (
<i className="fa fa-check-circle" data-test="success-icon"></i>
);
case NotificationBarType.Info:
return (
<i className="fa fa-info-circle" data-test="info-icon"></i>
);
case NotificationBarType.Disabled:
return (
<i className="fa fa-exclamation-circle" data-test="exclamation-icon"></i>
);
}
};
const NotificationBar: React.FC<INotificationBarProps> = (props) => {
const callDismissCallback = () => {
if (props.onButtonClick) {
props.onButtonClick();
}
};
const getbuttonText = () => {
if (props.isButtonPresent) {
return props.buttonText || 'Dismiss';
}
return '';
}
return (
<div className={'notification-bar alert alert-' + props.type} role="alert" data-test="notification-bar">
{getNotificationIcon(props.type, props.fontAwesomeIcon)}
{props.text}
{props.isButtonPresent ?
<button
type="button"
className="close"
data-dismiss="alert"
aria-label="Close"
onClick={callDismissCallback}
data-test="notification-bar-dismiss-button"
>
<span className="notification-bar-button-text">{getbuttonText()}</span>
</button>
: null}
{props.subText ?
<div className="notification-bar-sub-text-container">
<hr></hr>
<p><b>{props.subText}</b></p>
</div>
: null}
</div>
)
};
export default NotificationBar;
@@ -0,0 +1,140 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IAllowedStatusChange } from "../../reducers/SettingsReducer";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import StatusChangeSelector from "./StatusChangeSelector";
describe("AccountPageHeader", () => {
let wrapper: ShallowWrapper;
const selectedAccount: IRepoAccount = {
repoId: 476,
accountNo: "123456",
status: "Original Status",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
branch: 0,
origAccountTypeCode: "",
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 0,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
currentDelinquentAmount: 19.45,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [{
repoId: 1,
individualId: "1",
ownershipType: 6,
individualName: "John Doe",
}],
collaterals: [],
repoStatusChanges: [],
};
const allowedStatusChanges: IAllowedStatusChange[] = [
{
id: 1,
currentStatus: "Original Status",
allowedChangeStatus: "Secondary Status",
},
{
id: 2,
currentStatus: "Original Status",
allowedChangeStatus: "Secondary Status (Alternative)",
},
{
id: 3,
currentStatus: "Separated Status",
allowedChangeStatus: "Final Status",
},
];
let statusToUpdate: string;
beforeEach(() => {
statusToUpdate = "";
wrapper = shallow(
<StatusChangeSelector
selectedRepoAccount={selectedAccount}
allowedStatusChanges={allowedStatusChanges}
onStatusChange={(status) => statusToUpdate = status}
/>
);
});
it ("Renders Correct Status Options", () => {
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("Original Status");
});
it ("Renders Correct Status Options on Prop Change", () => {
wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
status: "Separated Status",
},
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("Separated Status");
});
it ("Renders Empty Options on Missing Account", () => {
wrapper.setProps({
selectedRepoAccount: undefined,
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("");
});
it ("Renders Single Unknown Status", () => {
wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
status: "Non-Existant Status",
},
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("Non-Existant Status");
});
it ("Select Callback Called", () => {
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusToUpdate).toEqual("");
statusChangeSelect.simulate("change", "Secondary Status");
expect(statusToUpdate).toEqual("Secondary Status");
});
it ("Does Not Render The Status Selector as Disabled When Account is Open", () => {
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.props().disabled).toBeFalsy();
});
it ("Renders Status Selector as Disabled When Account is Closed", () => {
wrapper = wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
dateClosed: moment("1916-04-29")
}
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.props().disabled).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
import React from "react";
import { getViableStatuses, IAllowedStatusChange } from "../../reducers/SettingsReducer";
import RepoAccountService from "../../services/RepoAccountService";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import FormInput, { FormInputType } from "../form-input/FormInput";
interface IStatusChangeSelectorProps {
selectedRepoAccount?: IRepoAccount;
allowedStatusChanges: IAllowedStatusChange[];
onStatusChange: (newStatus: string) => any;
}
const StatusChangeSelector: React.FC<IStatusChangeSelectorProps> = (props) => {
const getAllowedStatusChanges = (): string[] => {
if (!props.selectedRepoAccount) {
return [];
}
return getViableStatuses(props.selectedRepoAccount.status, props.allowedStatusChanges);
};
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedRepoAccount);
return (
<FormInput
type={FormInputType.Selection}
value={props.selectedRepoAccount ? props.selectedRepoAccount.status : ""}
onChange={props.onStatusChange}
disabled={!props.selectedRepoAccount || isAccountClosed || props.selectedRepoAccount.status.toUpperCase() === "REDEEMED"}
options={getAllowedStatusChanges().map((status) => {
return {
value: status,
display: status
};
})}
data-test="status-change-selector"
/>
);
};
export default StatusChangeSelector;
@@ -0,0 +1,7 @@
.nav-link {
cursor: pointer;
}
.nav-item p {
margin-bottom: 0;
}
@@ -0,0 +1,61 @@
import React from 'react';
import { ShallowWrapper, shallow } from "enzyme";
import TabView, { ITabViewPage as ITabViewPage } from "./TabView";
describe('TabView component', () => {
let wrapper: ShallowWrapper;
const pages: ITabViewPage[] = [
{
header: 'Page 1',
element: <div data-test="page-0-element"></div>,
},
{
header: 'Page 1',
element: <div data-test="page-1-element"></div>,
},
]
beforeEach(() => {
wrapper = shallow(<TabView pages={pages}/>);
})
it ('Renders initial page only', () => {
const pageZeroComponent = wrapper.find(`[data-test='page-0-element']`);
expect(pageZeroComponent.length).toBe(1);
const pageOneComponent = wrapper.find(`[data-test='page-1-element']`);
expect(pageOneComponent.length).toBe(0);
})
it ('Renders secondary page only on click', () => {
const navLinkComponent = wrapper.find(`[data-test='nav-link-1']`);
navLinkComponent.simulate('click');
const pageOneComponent = wrapper.find(`[data-test='page-1-element']`);
expect(pageOneComponent.length).toBe(1);
const pageZeroComponent = wrapper.find(`[data-test='page-0-element']`);
expect(pageZeroComponent.length).toBe(0);
})
it ('Calls Tab Callback on Tab Change', () => {
let tabOneValue = 0;
let tabTwoValue = 5;
const tabViewPages: ITabViewPage[] = [
{
header: 'Page 1',
element: <div data-test="page-0-element"></div>,
tabOpenCallback: () => {tabOneValue = 42},
},
{
header: 'Page 1',
element: <div data-test="page-1-element"></div>,
tabOpenCallback: () => {tabTwoValue = 1945}
},
];
wrapper = shallow(<TabView pages={tabViewPages} />);
expect(tabOneValue).toBe(42);
const navLinkComponent = wrapper.find(`[data-test='nav-link-1']`);
navLinkComponent.simulate('click');
expect(tabTwoValue).toBe(1945);
})
});
@@ -0,0 +1,76 @@
import React, { ReactElement } from "react";
import "./TabView.css";
export interface ITabViewPage {
header: string;
element: ReactElement;
tabOpenCallback?: () => any;
}
export interface TabViewProps {
pages: ITabViewPage[];
}
interface TabViewState {
currentPageIndex: number;
}
class TabView extends React.Component<TabViewProps, TabViewState> {
constructor(props: TabViewProps) {
super(props);
this.state = {
currentPageIndex: 0,
};
this.callPageCallback(0);
}
private getClassStyle(index: number): string {
return index === this.state.currentPageIndex ? "active ": "text-primary ";
}
private setCurrentPage(index: number): void {
const newState: TabViewState = Object.assign({}, this.state);
newState.currentPageIndex = index;
this.setState(newState);
this.callPageCallback(index);
}
private getCurrentPageElement(): ReactElement {
const currentPage = this.props.pages[this.state.currentPageIndex];
if (currentPage) {
return currentPage.element;
}
return (<h3>Missing</h3>);
}
private callPageCallback(index: number) {
if (index < this.props.pages.length) {
const tabPageCallback = this.props.pages[index].tabOpenCallback;
if (tabPageCallback !== undefined) {
tabPageCallback();
}
}
}
public render() {
return (
<div>
<ul className="nav nav-tabs">
{
this.props.pages.map((page, index) => {
return (
<li key={index} className="nav-item">
<p className={"nav-link " + this.getClassStyle(index)} onClick={() => this.setCurrentPage(index)} data-test={"nav-link-" + index}>{page.header}</p>
</li>
);
})
}
</ul>
{this.getCurrentPageElement()}
</div>
);
}
}
export default TabView;
@@ -0,0 +1,16 @@
.table-pagination p {
color: white;
margin: 0.5em 1em;
}
.table-pagination .table-chev:not(.table-chev-disabled) {
cursor: pointer;
}
.table-pagination .table-chev:hover:not(.table-chev-disabled) {
opacity: 0.5;
}
.table-pagination .table-chev-disabled {
opacity: 0.25;
}
@@ -0,0 +1,59 @@
import React from 'react';
import './TablePagination.css';
import ITablePagination from "./TablePagination";
import { shallow } from "enzyme";
import TablePagination from './TablePagination';
import { isExpressionWithTypeArguments } from 'typescript';
describe("FormInput Component", () => {
it ("Only Allows Next/Prev page when enabled 1", () => {
let nextPage = false;
let prevPage = false;
let wrapper = shallow(<TablePagination
totalItems = {10}
itemsPerPage = {2}
totalPages = {5}
currentPage = {0}
isPrevDisabled = {true}
isNextDisabled = {false}
nextPageCallback = {() => {nextPage = !nextPage}}
prevPageCallback = {() => {prevPage = !prevPage}}
/>)
const prevlink = wrapper.find(`[data-test='prev-page-link']`);
expect(prevlink.hasClass('table-chev-disabled')).toBe(true);
prevlink.simulate("click")
expect(prevPage).toBe(false)
const nextLink = wrapper.find(`[data-test='next-page-link']`);
expect(nextLink.hasClass('table-chev-disabled')).toBe(false);
nextLink.simulate("click")
expect(nextPage).toBe(true)
});
it ("Only Allows Next/Prev page when enabled 2", () => {
let nextPage = false;
let prevPage = false;
let wrapper = shallow(<TablePagination
totalItems = {10}
itemsPerPage = {2}
totalPages = {5}
currentPage = {2}
isPrevDisabled = {false}
isNextDisabled = {true}
nextPageCallback = {() => {nextPage = !nextPage}}
prevPageCallback = {() => {prevPage = !prevPage}}
/>)
const prevlink = wrapper.find(`[data-test='prev-page-link']`);
expect(prevlink.hasClass('table-chev-disabled')).toBe(false);
prevlink.simulate("click")
expect(prevPage).toBe(true)
const nextLink = wrapper.find(`[data-test='next-page-link']`);
expect(nextLink.hasClass('table-chev-disabled')).toBe(true);
nextLink.simulate("click")
expect(nextPage).toBe(false)
});
});
@@ -0,0 +1,51 @@
import React from 'react';
import './TablePagination.css';
interface ITablePaginationProps {
totalItems: number;
itemsPerPage: number;
currentPage: number;
totalPages: number;
isPrevDisabled: boolean;
isNextDisabled: boolean;
nextPageCallback: () => any;
prevPageCallback: () => any;
}
const TablePagination: React.FC<ITablePaginationProps> = (props) => {
const disabledChevClassName = 'table-chev-disabled';
return (
<div className="table-pagination bg-secondary">
<div className="row">
<div className="col">
<p
data-test='prev-page-link'
className={'table-chev table-chev-left ' + (props.isPrevDisabled ? disabledChevClassName : '')}
onClick={() => !props.isPrevDisabled ? props.prevPageCallback() : null}
>
<i className="fa fa-chevron-left" aria-hidden="true"></i>
</p>
</div>
<div className="col">
<p className="text-center">
Page <b>{props.currentPage}</b> of <b>{props.totalPages}</b>.
&nbsp;
Showing <b>{props.itemsPerPage}</b> of <b>{props.totalItems}</b>
</p>
</div>
<div className="col">
<p
data-test='next-page-link'
className={'text-right table-chev table-chev-right ' + (props.isNextDisabled ? disabledChevClassName : '')}
onClick={() => !props.isNextDisabled ? props.nextPageCallback() : null}
>
<i className="fa fa-chevron-right" aria-hidden="true"></i>
</p>
</div>
</div>
</div>
)
}
export default TablePagination;
@@ -0,0 +1,11 @@
.warning-info-page {
padding-top: 15em;
h1 {
text-align: center;
}
p {
font-weight: 100;
text-align: center;
}
}
@@ -0,0 +1,19 @@
import React from "react";
import "./WarningInfoPage.scss";
interface IWarningInfoPageProps {
title: string;
body?: string;
}
const WarningInfoPage: React.FC<IWarningInfoPageProps> = (props) => {
return (
<div className="warning-info-page">
<h1 aria-hidden="true"><i className="fa fa-exclamation-triangle" /></h1>
<h1>{props.title}</h1>
<p>{props.body}</p>
</div>
);
};
export default WarningInfoPage;