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,13 @@
import React from "react";
import WarningInfoPage from "../../components/warning-info-page/WarningInfoPage";
const AccessDeniedPage: React.FC = () => {
return (
<WarningInfoPage
title="Access Denied"
body="Please contact IT Operations if this is an error."
/>
);
};
export default AccessDeniedPage;
@@ -0,0 +1,9 @@
.account-activity {
padding-top: 1em;
}
.account-activity-notable-dates {
h5 {
margin-top: 0.5em;
}
}
@@ -0,0 +1,101 @@
import { shallow, ShallowWrapper } from "enzyme";
import { CustomAsyncNotification } from "lcu-react-lib";
import moment from "moment";
import React from "react";
import { HistorySource, HistoryType, ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
import { ICollateral, IRepoAccount, IRepoStatusChange } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {_AccountActivity} from "./AccountActivity";
describe("AccountActivity component", () => {
let wrapper: ShallowWrapper;
const accountHistories: IAsyncStateContainer<ITemenosHistory[]> = {
state: AsyncState.Success,
data: [{
Source: HistorySource.Account,
Name: "status",
Created: moment("1997-02-05"),
Type: HistoryType.Comment,
UserId: 0,
Message: "Mesage"
},
{
Source: HistorySource.Account,
Name: "status",
Created: moment("1997-02-05"),
Type: HistoryType.Comment,
UserId: 0,
Message: "Mesage"
},
{
Source: HistorySource.Account,
Name: "status",
Created: moment("1997-02-05"),
Type: HistoryType.Letter,
UserId: 0,
Message: "Mesage"
}]
};
const caseHistories: IAsyncStateContainer<ITemenosHistory[]> = {
state: AsyncState.Success,
data: []
};
beforeEach(() => {
wrapper = shallow(
<_AccountActivity
selectedAccount={{
accountNo: "1234",
collaterals: [] as ICollateral[],
repoStatusChanges: [] as IRepoStatusChange[],
} as IRepoAccount}
alerts={[]}
actions={[]}
markAlertRead={()=> {}}
accountHistories={accountHistories}
caseHistories={caseHistories}
saveRepoAccount={() => {}}
removeAccountFromAlertFilter={()=>{}}
externalUserMap={[]}
/>);
});
it("Displays all components", () => {
expect(wrapper.find(`[data-test='case-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='account-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='repo-letters']`).length).toBe(1);
});
it("Filters for only letters history for alerts and table", ()=>{
expect(wrapper.find(`[data-test='case-notification']`).prop("container").data.length).toBe(0);
expect(wrapper.find(`[data-test='account-notification']`).prop("container").data.length).toBe(1);
expect(wrapper.find(`[data-test='repo-letters']`).length).toBe(1);
});
it("Displays Custom Info Notification When Response Array is Emtpy", () => {
var customCaseNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='case-notification']`).prop("customNotifications");
expect(customCaseNotification).not.toBe(null);
expect(customCaseNotification[0].displayWhen(caseHistories.data)).toBe(true);
var customAccountNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='account-notification']`).prop("customNotifications");
expect(customAccountNotification).not.toBe(null);
expect(customAccountNotification[0].displayWhen(accountHistories.data)).toBe(false);
});
it("Displays Custom Info Notification When Response Array is Emtpy", () => {
//Previous Test With Data Flipped to ensure both have expected behaviour
var customCaseNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='case-notification']`).prop("customNotifications");
expect(customCaseNotification).not.toBe(null);
expect(customCaseNotification[0].displayWhen(accountHistories.data)).toBe(false);
var customAccountNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='account-notification']`).prop("customNotifications");
expect(customAccountNotification).not.toBe(null);
expect(customAccountNotification[0].displayWhen(caseHistories.data)).toBe(true);
});
});
@@ -0,0 +1,125 @@
import {AsyncNotification, NotificationType} from "lcu-react-lib";
import React from "react";
import { connect } from "react-redux";
import { markAlertRead } from "../../actions/AlertActions";
import { putRepoAccount, removeRepoAccountFromAlertFilter } from "../../actions/RepoAccountActions";
import { IAlert } from "../../reducers/AlertReducer";
import { IExternalUser } from "../../reducers/SettingsReducer";
import { HistoryType, ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import HistoryTable from "../history-table/HistoryTable";
import AccountActivityNotifications from "./account-activity-notifications/AccountActivityNotifications";
import "./AccountActivity.scss";
import NotableDates from "./notable-dates/NotableDates";
interface IAccountActivityProps {
selectedAccount: IRepoAccount;
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
externalUserMap: IExternalUser[];
}
interface IPropsFromState {
alerts: IAlert[];
actions: IAlert[];
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
alerts: state.alertState.alerts,
actions: state.alertState.actions,
};
};
interface IPropsFromDispatch {
markAlertRead: (alert: IAlert) => any;
saveRepoAccount: (repo: IRepoAccount) => any;
removeAccountFromAlertFilter: (alertMessage: string, repoId: number) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
markAlertRead: markAlertRead,
saveRepoAccount: putRepoAccount,
removeAccountFromAlertFilter: removeRepoAccountFromAlertFilter
};
type Props = IPropsFromState & IAccountActivityProps & IPropsFromDispatch;
const AccountActivity: React.FC<Props> = (props) => {
const filterOnlyAccountAlerts = (alerts: IAlert[]): IAlert[] => {
return alerts.filter((alert) => alert.repoId === props.selectedAccount.repoId);
};
const dismissAlert = (alert: IAlert) => {
props.markAlertRead(alert);
props.removeAccountFromAlertFilter(alert.message, alert.repoId);
};
const filterOnlyLettersHistories = (histories : IAsyncStateContainer<ITemenosHistory[]>) : IAsyncStateContainer<ITemenosHistory[]> => {
if(histories.data){
return {
state: histories.state,
data: histories.data.filter(x => x.Type === HistoryType.Letter)
};
}
return histories;
};
return (
<div className="account-activity container">
<AccountActivityNotifications
accountAlerts={filterOnlyAccountAlerts(props.alerts)}
accountActions={filterOnlyAccountAlerts(props.actions)}
onAlertDismiss={dismissAlert}
data-test="account-activity-notifications"
/>
<div className="row">
<div className="col-sm-8">
<h5>Repo History - Letters</h5>
<AsyncNotification
container={filterOnlyLettersHistories(props.accountHistories)}
subject={"Account Letter Histories"}
data-test={"account-notification"}
customNotifications={
[
{
displayWhen: (data: ITemenosHistory[]) => {return data.length < 1;},
message: `No Account Letters for ${props.selectedAccount.accountNo}`,
type: NotificationType.Info
}
]
}
/>
<AsyncNotification
container={filterOnlyLettersHistories(props.caseHistories)}
subject={"Case Letter Histories"}
data-test={"case-notification"}
customNotifications={
[
{
displayWhen: (data: ITemenosHistory[]) => {return data.length < 1;},
message: `No Case Letters for ${props.selectedAccount.accountNo}`,
type: NotificationType.Info
}
]
}
/>
<HistoryTable
accountNo={props.selectedAccount.accountNo}
caseHistories={filterOnlyLettersHistories(props.caseHistories)}
accountHistories={filterOnlyLettersHistories(props.accountHistories)}
externalUserMap={props.externalUserMap}
data-test="repo-letters"
/>
</div>
<NotableDates selectedAccount={props.selectedAccount} saveRepoAccount={props.saveRepoAccount} />
</div>
</div>
);
};
export const _AccountActivity = AccountActivity;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AccountActivity);
@@ -0,0 +1,44 @@
import React from 'react';
import { ShallowWrapper, shallow } from 'enzyme';
import AccountActivityNotifications from './AccountActivityNotifications';
import { IAlert } from '../../../reducers/AlertReducer';
describe('AccountActivityNotifications Component', () => {
let wrapper: ShallowWrapper;
const dummyAlerts: IAlert[] = [
{
id: 1,
message: "Message",
} as IAlert,
{
id: 1,
message: "Message",
} as IAlert,
];
const dummyActions: IAlert[] = [
{
id: 1,
message: "Message",
} as IAlert,
];
beforeEach(() => {
wrapper = shallow(
<AccountActivityNotifications
accountAlerts={dummyAlerts}
accountActions={dummyActions}
/>
);
});
it ('Renders Alerts', () => {
const alerts = wrapper.find(`[data-test='account-activity-notifications-alert']`);
expect(alerts.length).toBe(2);
});
it ('Renders Actions', () => {
const actions = wrapper.find(`[data-test='account-activity-notifications-action']`);
expect(actions.length).toBe(1);
});
});
@@ -0,0 +1,44 @@
import React from 'react';
import { IAlert } from '../../../reducers/AlertReducer';
import NotificationBar, { NotificationBarType } from '../../../components/notification-bar/NotificationBar';
interface IAcccountActivityNotificationsProps {
accountAlerts: IAlert[];
accountActions: IAlert[];
onAlertDismiss: (alert: IAlert) => any;
}
const AccountActivityNotifications: React.FC<IAcccountActivityNotificationsProps> = (props) => {
return (
<div className="account-activity-notifications">
{
props.accountActions.map((action) =>
<NotificationBar
key={action.id + '-' + action.message}
type={NotificationBarType.Info}
text={action.message}
subText={action.subMessage}
fontAwesomeIcon="fa-exclamation"
data-test="account-activity-notifications-action"
/>
)
}
{
props.accountAlerts.map((alert) =>
<NotificationBar
key={alert.id + '-' + alert.message}
type={NotificationBarType.Warning}
text={alert.message}
subText={alert.subMessage}
isButtonPresent={true}
onButtonClick={() => props.onAlertDismiss(alert)}
fontAwesomeIcon="fa-bell"
data-test="account-activity-notifications-alert"
/>
)
}
</div>
);
};
export default AccountActivityNotifications;
@@ -0,0 +1,11 @@
.notable-dates {
padding-top: 1em;
}
.notable-dates-edit {
margin-left: 2.5em;
}
.date-picker {
margin-top: 1em;
}
@@ -0,0 +1,76 @@
import React from "react";
import { ShallowWrapper, shallow } from "enzyme";
import NotableDates from "./NotableDates";
import { IRepoAccount, ICollateral, IRepoStatusChange } from '../../../services/service-types/RepoAccountTypes';
import moment from "moment";
describe('Notable Dates component', () => {
let wrapper: ShallowWrapper;
const dummyRepoAccount = {
accountNo: '1234',
collaterals: [] as ICollateral[],
repoStatusChanges: [] as IRepoStatusChange[],
} as IRepoAccount
let dummyUpdatedRepoAccount: IRepoAccount | undefined;
beforeEach(() => {
wrapper = shallow(
<NotableDates
selectedAccount={dummyRepoAccount}
saveRepoAccount={(x) => dummyUpdatedRepoAccount = x}
/>);
})
it ('Renders Existing Dates', () => {
wrapper = wrapper.setProps({
selectedAccount: {
o4RDate: moment('1945-05-07'),
redemptionExpiredDate: moment('1945-09-02'),
collaterals: [
{},
{ retitledInLandmarkName: moment('1914-07-28') },
],
repoStatusChanges: [
{ statusTo: 'SOLD', changedDate: moment('1865-04-12') },
{ statusTo: 'REPO', changedDate: moment('1918-11-11') }
]
}
});
const o4rDate = wrapper.find(`[data-test='notable-dates-o4r-date']`);
expect(o4rDate.length).toBe(1);
expect(o4rDate.text()).toContain('05/07/1945');
const repoDate = wrapper.find(`[data-test='notable-dates-repo-date']`);
expect(repoDate.length).toBe(1);
expect(repoDate.text()).toContain('11/11/1918');
const redemDate = wrapper.find(`[data-test='notable-dates-redem-exp-date']`);
expect(redemDate.length).toBe(1);
expect(redemDate.text()).toContain('09/02/1945');
const retitledDate = wrapper.find(`[data-test='notable-dates-retitled-date']`);
expect(retitledDate.length).toBe(1);
expect(retitledDate.text()).toContain('07/28/1914');
});
it ('Renders Non-Existing Dates', () => {
const o4rDate = wrapper.find(`[data-test='notable-dates-o4r-date']`);
expect(o4rDate.length).toBe(1);
expect(o4rDate.text()).toContain('N/A');
const repoDate = wrapper.find(`[data-test='notable-dates-repo-date']`);
expect(repoDate.length).toBe(1);
expect(repoDate.text()).toContain('N/A');
const redemDate = wrapper.find(`[data-test='notable-dates-redem-exp-date']`);
expect(redemDate.length).toBe(1);
expect(redemDate.text()).toContain('N/A');
const retitledDate = wrapper.find(`[data-test='notable-dates-retitled-date']`);
expect(retitledDate.length).toBe(1);
expect(retitledDate.text()).toContain('N/A');
});
});
@@ -0,0 +1,121 @@
import React from "react";
import { ICollateral, IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import RepoAccountService from "../../../services/RepoAccountService";
import { Moment } from "moment";
import DateFormatting from "../../../shared/formatting/DateFormatting";
import DatePicker from 'react-datepicker';
import DateConversions from "../../../shared/conversions/DateConversions";
import "./NotableDates.css";
const cloneDeep = require('lodash.clonedeep');
interface INotableDatesProps {
selectedAccount: IRepoAccount;
saveRepoAccount: (repo: IRepoAccount) => any;
}
interface INotableDatesState {
repoAccount: IRepoAccount;
isOpen: boolean;
}
type IRepoAccountUpdater<T> = (repoAccount: IRepoAccount, value: T) => void;
class NotableDates extends React.Component<INotableDatesProps, INotableDatesState> {
constructor(props: INotableDatesProps) {
super(props);
this.state = {
repoAccount: cloneDeep(props.selectedAccount),
isOpen: false
}
}
private applyChange<T>(value: T, updater: IRepoAccountUpdater<T>) {
const copiedAccount = cloneDeep(this.state.repoAccount);
updater(copiedAccount, value);
this.setState({
repoAccount: copiedAccount,
});
}
private handleDateChange(date: Date | null, updater: IRepoAccountUpdater<Moment | undefined>) {
const momentDate = DateConversions.dateToMoment(date);
this.applyChange(momentDate, updater);
}
toggleOpen = () => this.setState({ isOpen: !this.state.isOpen });
private formatDate = (date: Moment | undefined = undefined): string => {
return date ? DateFormatting.FormatMMDDYYYY(date) : "N/A";
};
private getRetitledDateString = (collaterals: ICollateral[]): string => {
for (const collateral of collaterals) {
if (collateral.retitledInLandmarkName) {
return this.formatDate(collateral.retitledInLandmarkName);
}
}
return this.formatDate();
};
private getRepoDate = (repoAccount: IRepoAccount): string => {
return this.formatDate(RepoAccountService.GetLatestStatusChangeDate("REPO", repoAccount));
};
private editButton = () => {
return (
<button
className="notable-dates-edit btn btn-info"
onClick={this.toggleOpen}
data-test="notable-dates-edit-retitled-date-btn"
>
Edit
</button>
);
};
render() {
return (
<div className="col-sm-4 card notable-dates ml">
<h5>Notable Dates:</h5>
<hr />
<p><b>O4R:</b>
<span data-test="notable-dates-o4r-date"> {this.formatDate(this.props.selectedAccount.o4RDate)}</span>
</p>
<p><b>Repo:</b>
<span data-test="notable-dates-repo-date"> {this.getRepoDate(this.props.selectedAccount)}</span>
</p>
<p><b>Redemption Expired:</b>
<span data-test="notable-dates-redem-exp-date"> {this.formatDate(this.props.selectedAccount.redemptionExpiredDate)}
</span>
{this.state.isOpen ?
<div>
<div className="form-group">
<DatePicker
className="form-control date-picker"
placeholderText="mm/dd/yyyy"
selected={DateConversions.momentToDate(this.state.repoAccount.redemptionExpiredDate)}
onChange={(date) => this.handleDateChange(date, (a, v) => a.redemptionExpiredDate = v)}
data-test={"notable-dates-redem-exp-date-picker"}
/>
</div>
<div className="form-group">
<button
className="form-control btn btn-info auction-panel-btn"
onClick={() => { this.props.saveRepoAccount(this.state.repoAccount) }}
data-test="notable-dates-redem-exp-submit-btn">
Submit
</button>
</div>
</div>
: null}
</p>
<p><b>Retitled:</b>
<span data-test="notable-dates-retitled-date"> {this.getRetitledDateString(this.props.selectedAccount.collaterals)}</span>
</p>
</div>
);
}
}
export default NotableDates;
@@ -0,0 +1,229 @@
import {shallow, ShallowWrapper} from "enzyme";
import moment, {Moment} from "moment";
import React from "react";
import {IRepoAccount} from "../../services/service-types/RepoAccountTypes";
import DateCalculations from "../../shared/calculations/DateCalculations";
import AccountDetails from "./AccountDetails";
describe("AccountDetails Container", () => {
let wrapper: ShallowWrapper;
const dummyAccount: IRepoAccount = {
repoId: 476,
accountNo: "string",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
origAccountTypeCode: "",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 19.45,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
writtenOffDate: moment("1492-01-01"),
borrowers: [],
collaterals: [],
repoStatusChanges: [],
};
beforeEach(() => {
wrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
});
it("Renders Select Account Fields Correctly", () => {
const repoDbDate = wrapper.find(`[data-test='repo-db-date']`);
const firstDelinquent = wrapper.find(`[data-test='first-delinquent']`);
const otherRepos = wrapper.find(`[data-test='other-repos']`);
const daysDelqWhenAdded = wrapper.find(`[data-test='days-delq-when-added']`);
const origAmount = wrapper.find(`[data-test='orig-amount']`);
const balance = wrapper.find(`[data-test='balance']`);
const lastPayment = wrapper.find(`[data-test='last-payment']`);
const daysInStatus = wrapper.find(`[data-test='days-in-status']`);
const daysCurrentDelq = wrapper.find(`[data-test='days-current-delq']`);
const accountCode = wrapper.find(`[data-test='account-code']`);
const branch = wrapper.find(`[data-test='branch']`);
const currentDelqAmount = wrapper.find(`[data-test='current-delq-amount']`);
const lastPmtAmount = wrapper.find(`[data-test='last-pmt-amount']`);
const requiredAmount = wrapper.find(`[data-test='required-amount']`);
const bankruptcy = wrapper.find(`[data-test='bankruptcy']`);
expect(repoDbDate.length).toBe(1);
expect(repoDbDate.props().value).toEqual("05/07/1945");
expect(firstDelinquent.length).toBe(1);
expect(firstDelinquent.props().value).toEqual("11/11/1918");
expect(otherRepos.length).toBe(1);
expect(otherRepos.props().value).toEqual("0");
expect(daysDelqWhenAdded.length).toBe(1);
expect(daysDelqWhenAdded.props().value).toEqual(101);
expect(origAmount.length).toBe(1);
expect(origAmount.props().value).toEqual("$42.00");
expect(balance.length).toBe(1);
expect(balance.props().value).toEqual("$0.00");
expect(lastPayment.length).toBe(1);
expect(lastPayment.props().value).toEqual("04/15/1865");
expect(daysInStatus.length).toBe(1);
expect(daysInStatus.props().value).toEqual("Missing");
expect(daysCurrentDelq.length).toBe(1);
expect(daysCurrentDelq.props().value).toEqual(
DateCalculations.DayDifference(moment("1941-12-07"), moment())
);
expect(accountCode.length).toBe(1);
expect(accountCode.props().value).toEqual("(-) / acct-type-code");
expect(branch.length).toBe(1);
expect(branch.props().value).toEqual(0);
expect(currentDelqAmount.length).toBe(1);
expect(currentDelqAmount.props().value).toEqual("$19.45");
expect(lastPmtAmount.length).toBe(1);
expect(lastPmtAmount.props().value).toEqual("$1,912.00");
expect(requiredAmount.length).toBe(1);
expect(requiredAmount.props().value).toEqual("$3.14");
expect(bankruptcy.length).toBe(1);
expect(bankruptcy.props().value).toEqual("No");
});
it("Written Off Date Displays No", () => {
const dummyAccount: IRepoAccount = {
repoId: 476,
accountNo: "string",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
origAccountTypeCode: "",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 19.45,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [],
collaterals: [],
repoStatusChanges: [],
};
wrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const writtenOffDate = wrapper.find(`[data-test='written-off']`);
expect(writtenOffDate.length).toBe(1);
expect(writtenOffDate.props().value).toBe("No");
});
it("Written Off Date Displays Date", () => {
wrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const writtenOffDate = wrapper.find(`[data-test='written-off']`);
expect(writtenOffDate.length).toBe(1);
expect(writtenOffDate.props().value).toBe("01/01/1492");
});
it("Renders Account Code with Original Account Code Properly", () => {
dummyAccount.origAccountTypeCode = "orig-code";
const tempwrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const accountCode = tempwrapper.find(`[data-test='account-code']`);
expect(accountCode.props().value).toBe("(orig-code) / acct-type-code");
});
it("Renders the Branch Name when a mapping is available", () => {
const tempwrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[{branchId: 0, branchName: "Milwaukee"}]}
refreshCollateral={() => {
}}
/>
);
const branch = tempwrapper.find(`[data-test='branch']`);
expect(branch.props().value).toBe("Milwaukee");
});
it("Renders the Branch Number when branchMapping is undefined", () => {
const tempwrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const branch = tempwrapper.find(`[data-test='branch']`);
expect(branch.props().value).toBe(0);
});
});
@@ -0,0 +1,170 @@
import moment from "moment";
import React from "react";
import CollateralDisplay, { ICollateralDisplay } from "../../components/collateral-display/CollateralDisplay";
import { IBranchSetEntry } from "../../reducers/SettingsReducer";
import RepoAccountService from "../../services/RepoAccountService";
import { ICollateral, IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import DateCalculations from "../../shared/calculations/DateCalculations";
import DateFormatting from "../../shared/formatting/DateFormatting";
import MonetaryFormatting from "../../shared/formatting/MonetaryFormatting";
interface IAccountDetailsProps {
selectedAccount?: IRepoAccount;
otherRepos: string;
refreshCollateral: (collateral: ICollateral, memberNo: number, loanNo: number) => any;
branchMapping: IBranchSetEntry[];
}
const AccountDetails: React.FC<IAccountDetailsProps> = (props) => {
const getWrittenOffText = () => {
const writtenOffDate = getAccountField(acc => acc.writtenOffDate);
if (writtenOffDate) {
return DateFormatting.FormatMMDDYYYY(writtenOffDate);
}
return "No";
};
const getAccountField = function<T>(valuePlucker: (repoAccount: IRepoAccount) => T): T | undefined {
if (props.selectedAccount) {
return valuePlucker(props.selectedAccount);
}
};
const getControlledAccountField = function<T>(valuePlucker: (repoAccount: IRepoAccount) => T): T | string {
const pluckedVal = getAccountField(valuePlucker);
return pluckedVal !== undefined ? pluckedVal : "";
};
const getDaysCurrentDelq = (): string | number => {
const dueToPayDate = getAccountField(acc => acc.dueToPayDate);
if (dueToPayDate) {
return DateCalculations.DayDifference(dueToPayDate, moment());
}
return "";
};
const formatAccountTypeCode = (): string => {
const accountTypeCode = getControlledAccountField(acc => acc.accountTypeCode);
let origAccountTypeCode = getControlledAccountField(acc => acc.origAccountTypeCode);
origAccountTypeCode = !origAccountTypeCode ? "-" : origAccountTypeCode;
return "("+origAccountTypeCode+") / " + accountTypeCode;
};
const createCollateralDisplays = (): ICollateralDisplay[] => {
if (props.selectedAccount) {
return props.selectedAccount.collaterals.map((collateral) => {
return {
collateral: collateral,
refreshCollateral: () => {
return props.refreshCollateral(collateral, props.selectedAccount!.memberNo, props.selectedAccount!.loanNo);
},
};
});
} else {
return [];
}
};
const getBranchName = (): string | number =>{
const BranchIdFromAccount = getControlledAccountField(acc => acc.branch);
if(!props.branchMapping){
return BranchIdFromAccount;
}
// eslint-disable-next-line eqeqeq
const mappedEntry = props.branchMapping.find(branch => branch.branchId == BranchIdFromAccount);
return (mappedEntry && mappedEntry.branchName) ? mappedEntry.branchName : BranchIdFromAccount;
};
const getSixtyPlusCollector = (): string | undefined =>{
const SixtyPlusCollector = getAccountField(acct => acct.sixtyPlusCollector);
if(SixtyPlusCollector){
return SixtyPlusCollector.sixtyPlusCollectorName;
}
return "";
}
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedAccount);
return (
<div className="account-details-holder container">
<CollateralDisplay isAccountClosed={isAccountClosed} collateral={createCollateralDisplays()} />
<div className="account-page-input-section row">
<div className="col-sm">
<p>Repo DB Date</p>
<input data-test="repo-db-date" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.o4RDate))} disabled={true} />
<p>Orig Amount</p>
<input data-test="orig-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.origAmount))} disabled={true} />
<p>Account Code</p>
<input data-test="account-code" type="text" className="form-control" value={formatAccountTypeCode()} disabled={true} />
<p>Bankruptcy</p>
<input data-test="bankruptcy" type="text" className="form-control" value={getAccountField(acc => acc.bankruptcy) || "No"} disabled={true} />
</div>
<div className="col-sm">
<p>First Delinquent</p>
<input data-test="first-delinquent" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.dateFirstDelinquent))} disabled={true} />
<p>Balance</p>
<input data-test="balance" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.balance))} disabled={true} />
<p>Branch</p>
<input data-test="branch" type="text" className="form-control" value={getBranchName()} disabled={true} />
<p>Temenos Account</p>
<input data-test="reference-id" type="text" className="form-control" value={getAccountField(acc => acc.referenceId)} disabled={true}/>
</div>
<div className="col-sm">
<p>Written Off</p>
<input data-test="written-off" type="text" className="form-control" value={getWrittenOffText()} disabled={true} />
<p>Last Payment</p>
<input data-test="last-payment" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.lastPaymentDate))} disabled={true} />
<p>Current Delq Amount</p>
<input data-test="current-delq-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.delinquentAmount))} disabled={true} />
<p>Temenos Case</p>
<input data-test="reference-case-id" type="text" className="form-control" value={getAccountField(acc => acc.referenceCaseId)} disabled={true}/>
</div>
<div className="col-sm">
<p>Other Repos</p>
<input data-test="other-repos" type="text" className="form-control" value={props.otherRepos} disabled={true} />
<p>Days In Status</p>
<input data-test="days-in-status" type="text" className="form-control" value={RepoAccountService.GetDaysInStatus(props.selectedAccount)} disabled={true} />
<p>Last Pmt Amount</p>
<input data-test="last-pmt-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.lstPmtAmt))} disabled={true} />
<p>Account Last Processed</p>
<input data-test="account-last-proccessed" type="text" className="form-control" value={DateFormatting.FormatMMDDYYYHHMMA(getAccountField(acc => acc.accountLastProcessed ))} disabled={true} />
</div>
<div className="col-sm">
<p>Days Delq When Added</p>
<input data-test="days-delq-when-added" type="text" className="form-control" value={getControlledAccountField(acc => acc.daysDelinquent)} disabled={true} />
<p>Days Current Delq</p>
<input data-test="days-current-delq" type="text" className="form-control" value={getDaysCurrentDelq()} disabled={true} />
<p>Required Amount</p>
<input data-test="required-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.requiredPaymentAmount))} disabled={true} />
<p>60+ User Assigned</p>
<input data-test="sixty-plus-collector" type="text" className="form-control" value={getSixtyPlusCollector()} disabled={true} />
</div>
</div>
</div>
);
};
export default AccountDetails;
@@ -0,0 +1,65 @@
import React from 'react';
import {_AccountListView} from "./AccountListView";
import AsyncState from '../../shared/enumerations/AsyncState';
import { ShallowWrapper, shallow, mount } from 'enzyme';;
describe('AccountListView Container', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<_AccountListView
repoAccounts={{
state: AsyncState.Inactive,
data: []
}}
filteredAccountIds={{ state: AsyncState.Inactive }}
accountFilters={{}}
goToNextRepoAccountPage={() => {}}
goToPreviousRepoAccountPage={() => {}}
history={{} as any}
location={{} as any}
match={{} as any}
/>
);
});
it('Renders AccountListPagedTable', () => {
var tempWrapper = mount(
<_AccountListView
repoAccounts={{
state: AsyncState.Success,
data: []
}}
filteredAccountIds={{ state: AsyncState.Inactive }}
accountFilters={{}}
goToNextRepoAccountPage={() => {}}
goToPreviousRepoAccountPage={() => {}}
history={{} as any}
location={{} as any}
match={{} as any}
/>
);
const component = tempWrapper.find('AccountListPagedTable');
expect(component.length).toBe(1);
});
it ('Renders Non-Success Notification Bar', () => {
const nonSuccessNotificationBar = wrapper.find(`[data-test='notification-bar-non-success']`);
expect(nonSuccessNotificationBar.length).toBe(1);
});
it ('Renders "No Accounts Found" Notification', () => {
wrapper = wrapper.setProps({
repoAccounts: {
state: AsyncState.Success,
data: []
}
});
const noAccountsNotificationBar = wrapper.find(`[data-test='notification-bar-no-accounts']`);
expect(noAccountsNotificationBar.length).toBe(1);
});
});
@@ -0,0 +1,83 @@
import React from "react";
import {connect} from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { goToNextRepoAccountPage, goToPreviousRepoAccountPage } from "../../actions/RepoAccountActions";
import NotificationBar, { NotificationBarType } from "../../components/notification-bar/NotificationBar";
import { getCurrentlyFilteredAccounts } from "../../selectors/RepoAccountSelectors";
import { IRepoAccount, IRepoAccountFilters } from "../../services/service-types/RepoAccountTypes";
import AsyncState, { mapAsyncStateToNotificationType } from "../../shared/enumerations/AsyncState";
import Routes from "../../shared/Routes";
import IAsyncStateContainer, { IAsyncStateContainerStrict } from "../../shared/types/IAsyncStateContainer";
import { IPagedResponse } from "../../shared/types/IPagedResponse";
import {IAppState} from "../../store/Store";
import AccountListPagedTable from "./account-list-paged-table/AccountListPagedTable";
interface IPropsFromState {
filteredAccountIds: IAsyncStateContainer<IPagedResponse<number>>;
repoAccounts: IAsyncStateContainerStrict<IRepoAccount[]>;
accountFilters: IRepoAccountFilters;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoAccounts: getCurrentlyFilteredAccounts(state),
accountFilters: state.repoAccountState.accountFilters,
filteredAccountIds: state.repoAccountState.filteredAccounts
};
};
interface IDispatchFromState {
goToNextRepoAccountPage: () => any;
goToPreviousRepoAccountPage: () => any;
}
const mapDispatchToProps: IDispatchFromState = {
goToNextRepoAccountPage: goToNextRepoAccountPage,
goToPreviousRepoAccountPage: goToPreviousRepoAccountPage
};
type Props = IPropsFromState & RouteComponentProps & IDispatchFromState;
export const AccountListView: React.FC<Props> = (props: Props) => {
const goToAccount = (account: IRepoAccount) => {
props.history.push(Routes.Account(account.repoId));
};
return (
<div>
{
props.repoAccounts.state !== AsyncState.Success
?
<NotificationBar
type={mapAsyncStateToNotificationType(props.repoAccounts.state)}
text={props.repoAccounts.state}
data-test="notification-bar-non-success"
/>
:
<AccountListPagedTable
repoAccounts={props.repoAccounts.data}
pagedResponse={props.filteredAccountIds.data}
onRepoAccountClicked={goToAccount}
onPrevCallback={props.goToPreviousRepoAccountPage}
onNextCallback={props.goToNextRepoAccountPage}
/>
}
{props.repoAccounts.state === AsyncState.Success && props.repoAccounts.data.length === 0 ?
<NotificationBar
type={NotificationBarType.Warning}
text="No Accounts Found"
data-test="notification-bar-no-accounts"
/>
: null}
</div>
);
};
export const _AccountListView = AccountListView;
// This should be ideally done with componse.
// However, the type wasn't being properly inferred.
// This may be something worth looking into at a later point
export default withRouter(connect<IPropsFromState, IDispatchFromState, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AccountListView));
@@ -0,0 +1,38 @@
import React from "react";
import TablePagination from "../../../components/table-pagination/TablePagination";
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import { IPagedResponse } from "../../../shared/types/IPagedResponse";
import AccountListTable from "../account-list-table/AccountListTable";
interface IAccountListPagedTableProps {
repoAccounts: IRepoAccount[];
pagedResponse?: IPagedResponse<any>;
onRepoAccountClicked: (repoAccount: IRepoAccount) => any;
onPrevCallback: () => any;
onNextCallback: () => any;
}
const AccountListPagedTable: React.FC<IAccountListPagedTableProps> = (props) => {
return (
<div className="account-list-paged-table">
<div className="account-table-wrapper">
<AccountListTable
repoAccounts={props.repoAccounts}
onRepoAccountClicked={props.onRepoAccountClicked}
/>
</div>
<TablePagination
totalItems={props.pagedResponse ? props.pagedResponse.totalItems : 0}
itemsPerPage={props.pagedResponse ? props.pagedResponse.items.length : 0}
currentPage={props.pagedResponse ? props.pagedResponse.currentPage : 1}
totalPages={props.pagedResponse ? props.pagedResponse.totalPages : 1}
isPrevDisabled={props.pagedResponse ? !props.pagedResponse.previousPage : false}
isNextDisabled={props.pagedResponse ? !props.pagedResponse.nextPage : false}
prevPageCallback={props.onPrevCallback}
nextPageCallback={props.onNextCallback}
/>
</div>
);
};
export default AccountListPagedTable;
@@ -0,0 +1,37 @@
.account-list-table {
.main-panel {
padding: 75px 15px;
min-height: calc(101vh - 80px);
}
table.lcu {
display: flex;
flex-flow: column;
width: 100%;
}
thead.lcu {
flex: 0 0 auto;
}
tbody.lcu {
flex: 1 1 auto;
display: block;
overflow-y: auto;
overflow-x: hidden;
}
tr.lcu {
width: 100%;
display: table;
table-layout: fixed;
}
tr, tr * {
user-select: none;
}
.account-no-label {
margin-bottom: 0;
}
}
@@ -0,0 +1,65 @@
import React from "react";
import CollateralTextSummary from "../../../components/collateral/text-summary/CollateralTextSummary";
import RepoAccountService from "../../../services/RepoAccountService";
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import MonetaryFormatting from "../../../shared/formatting/MonetaryFormatting";
import StringFormatting from "../../../shared/formatting/StringFormatting";
import "./AccountListTable.scss";
interface IAccountListTableProps {
repoAccounts: IRepoAccount[];
onRepoAccountClicked: (repoAccount: IRepoAccount) => any;
}
export const AccountListTable: React.FC<IAccountListTableProps> = (props) => {
return (
<table className="account-list-table table table-striped table-hover">
<thead className="thead-dark">
<tr>
<th scope="col">AccountNo</th>
<th scope="col">Borrower</th>
<th scope="col">Collateral</th>
<th scope="col">LoanBalance</th>
<th scope="col">Status</th>
<th scope="col">Days In Status</th>
</tr>
</thead>
<tbody>
{props.repoAccounts.map((repoAccount, _) => {
return (
<tr key={repoAccount.repoId} onClick={() => props.onRepoAccountClicked(repoAccount)}>
<td>
<p className="account-no-label">{repoAccount.accountNo}</p>
{RepoAccountService.IsAccountClosed(repoAccount) ?
<p className="text-danger"><b>CLOSED</b></p>
: null}
</td>
<td>{
repoAccount.borrowers.map((borrower) => {
return (
<p key={borrower.individualId}>{StringFormatting.ToTitleCase(borrower.individualName)}</p>
);
})
}</td>
<td>{
repoAccount.collaterals.map((collateral) => {
return (
<p key={collateral.collateralId}>
<CollateralTextSummary collateral={collateral}/>
</p>
);
})
}</td>
<td><p>{MonetaryFormatting.FormatUSD(repoAccount.balance)}</p></td>
<td><p>{repoAccount.status}</p></td>
<td><p>{RepoAccountService.GetDaysInStatus(repoAccount)}</p></td>
</tr>
);
})}
</tbody>
</table>
);
};
export default AccountListTable;
@@ -0,0 +1,49 @@
.account-details-holder p {
margin-bottom: 0;
}
.account-details-holder input {
margin-bottom: 1em;
}
.tab-view-wrapper {
margin-top: 2em;
}
.account-page-input-section p, .account-page-input-section input {
font-size: .9em;
}
.account-page-input-section input {
height: 2em;
}
.back-navigation-header .fa-chevron-left {
cursor: pointer;
margin-right: .5em;
}
.back-navigation-header * {
text-align: center;
margin-bottom: 0;
}
.back-navigation-header .row .col-sm:first-child * {
text-align: left;
}
.back-navigation-header .row .col-sm:last-child * {
text-align: right;
}
@media (max-width: 575px) {
.back-navigation-header * {
text-align: left;
}
.back-navigation-header .row .col-sm:last-child * {
text-align: left;
}
.account-page-container .col-sm:not(:first-child) {
padding-top: 1em;
}
}
@@ -0,0 +1,94 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import { _AccountPage } from "./AccountPage";
{/* - 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("AccountPage component", () => {
let wrapper: ShallowWrapper;
const dummy = {} as any;
const matchParams = {
params: {
accountno: "123456",
},
} as any;
const selectedAccount: IRepoAccount = {
repoId: 476,
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: [],
}
beforeEach(() => {
wrapper = shallow(
<_AccountPage
repoAccounts={[selectedAccount]}
allowedStatusChanges={[]}
match={matchParams}
history={dummy}
location={dummy}
putRepoAccount={dummy}
refreshCollateral={() => {}}
selectedAccount={{
state: AsyncState.Inactive
}}
filteredAccounts={{
state: AsyncState.Inactive
}}
fetchSelectedRepoAccount={() => {}}
searchRepoAccounts={() => {}}
getInitialRepoAccounts={() => {}}
/>
);
});
it ("Renders Account Header", () => {
const component = wrapper.find(`[data-test='account-page-header']`);
expect(component.length).toBe(1);
});
it ("Renders Account Details", () => {
const accountDetails = wrapper.find(`[data-test='account-page-details']`);
expect(accountDetails.length).toBe(1);
});
it ("Renders the Tab View", () => {
const tabView = wrapper.find(`[data-test='account-page-tab-view']`);
expect(tabView.length).toBe(1);
});
});
*/}
@@ -0,0 +1,182 @@
import Axios from "axios";
import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps } from "react-router";
import { toast } from "react-toastify";
import { compose } from "redux";
import { fetchSelectedRepoAccount, putRepoAccount, refreshCollateral } from "../../actions/RepoAccountActions";
import AccountPageHeader from "../../components/account-page-header/AccountPageHeader";
import { IAllowedStatusChange, IBranchSetEntry } from "../../reducers/SettingsReducer";
import { getSelectedAccount } from "../../selectors/RepoAccountSelectors";
import { getBranchMapping } from "../../selectors/SettingsSelector";
import { ICollateral, IRepoAccount, IRepoAccountMap } from "../../services/service-types/RepoAccountTypes";
import Configuration from "../../shared/configuration/Configuration";
import AsyncState from "../../shared/enumerations/AsyncState";
import AxiosMessageFormatting from "../../shared/formatting/AxiosMessageFormatting";
import UrlFormatting from "../../shared/formatting/UrlFormatting";
import ToastDefaults from "../../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AccountDetails from "../account-details/AccountDetails";
import AccountPageTabView from "./account-page-tab-view/AccountPageTabView";
import "./AccountPage.css";
const cloneDeep = require("lodash.clonedeep");
export interface IAccountPageRouterProps {
readonly repoId: string;
}
interface IPropsFromState {
readonly repoAccounts: IRepoAccountMap;
readonly allowedStatusChanges: IAllowedStatusChange[];
readonly selectedAccount: IAsyncStateContainer<IRepoAccount>;
readonly branchMap: IBranchSetEntry[];
}
interface IPropsFromDispatch {
readonly refreshCollateral: (collateral: ICollateral, memberNo: number, loanNo: number) => any;
readonly putRepoAccount: (repoAccount: IRepoAccount) => any;
readonly fetchSelectedRepoAccount: (repoId: number) => any;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoAccounts: state.repoAccountState.repoAccounts,
allowedStatusChanges: state.settingsState.allowedStatusChanges,
selectedAccount: getSelectedAccount(state),
branchMap: getBranchMapping(state)
};
};
const mapDispatchToProps: IPropsFromDispatch = {
fetchSelectedRepoAccount: fetchSelectedRepoAccount,
refreshCollateral: refreshCollateral,
putRepoAccount: putRepoAccount
};
interface IAccountPageState {
numberOfOtherRepos: IAsyncStateContainer<number>;
}
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps<IAccountPageRouterProps>;
class AccountPage extends React.Component<Props, IAccountPageState> {
constructor(props: Props) {
super(props);
this.state = {
numberOfOtherRepos: { state: AsyncState.Loading }
};
this.returnToHomePage = this.returnToHomePage.bind(this);
this.saveRepoAccountStatus = this.saveRepoAccountStatus.bind(this);
}
componentWillMount() {
this.props.fetchSelectedRepoAccount(parseInt(this.props.match.params.repoId));
}
componentDidUpdate(prevProps: Props) {
if (
prevProps.selectedAccount.state !== this.props.selectedAccount.state &&
this.props.selectedAccount.state === AsyncState.Success &&
this.props.selectedAccount.data
) {
this.loadOtherRepos(this.props.selectedAccount.data.memberNo);
}
}
async loadOtherRepos(memberNo: number) {
try {
// This could possibly return false positives. At some point, a new endpoint should be created
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_search),
{ key: "searchPhrase", value: memberNo }
);
const result = await Axios.get(parameterizedUrl);
const historyResult = result.data as IRepoAccount[];
this.setState({
numberOfOtherRepos: {
data: historyResult.length - 1,
state: AsyncState.Success,
},
});
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
this.setState({
numberOfOtherRepos: {
state: AsyncState.Failure,
},
});
}
}
saveRepoAccountStatus(newStatus: string, selectedRepoAccount?: IRepoAccount): void {
if (selectedRepoAccount) {
const copiedRepoAccount = cloneDeep(selectedRepoAccount) as IRepoAccount;
copiedRepoAccount.status = newStatus;
this.props.putRepoAccount(copiedRepoAccount);
}
}
returnToHomePage() {
this.props.history.goBack();
}
getAccountNoHeader(selectedAccount: IAsyncStateContainer<IRepoAccount>): string {
if (selectedAccount.state === AsyncState.Success) {
return selectedAccount.data ? selectedAccount.data.accountNo : "-";
}
return selectedAccount.state;
}
getOtherRepos(): string {
if (this.state.numberOfOtherRepos.state === AsyncState.Success && this.state.numberOfOtherRepos.data !== undefined) {
return (this.state.numberOfOtherRepos.data).toString();
} else {
return this.state.numberOfOtherRepos.state;
}
}
render() {
return (
<div className="account-page-container container-fluid main-panel" data-test="mainPanel">
<AccountPageHeader
accountNoHeader={this.getAccountNoHeader(this.props.selectedAccount)}
selectedRepoAccount={this.props.selectedAccount.data}
allowedStatusChanges={this.props.allowedStatusChanges}
onBackButtonClicked={this.returnToHomePage}
onStatusChange={(newStatus) => this.saveRepoAccountStatus(newStatus, this.props.selectedAccount.data)}
putRepoAccount={this.props.putRepoAccount}
data-test="account-page-header"
/>
<AccountDetails
selectedAccount={this.props.selectedAccount.data}
otherRepos={this.getOtherRepos()}
refreshCollateral={this.props.refreshCollateral}
branchMapping={this.props.branchMap}
data-test="account-page-details"
/>
<div className="tab-view-wrapper">
<AccountPageTabView
selectedAccount={this.props.selectedAccount.data}
data-test="account-page-tab-view"
/>
</div>
</div>
);
}
}
// https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability
// Use a function composition utility: compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
// connect<ReduxInjected, DispatchInjected, GivenByParentWhenUsingThisComponent, ReduxStoreType>
connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)
);
const EnhancedComponent = enhance(AccountPage);
export const _AccountPage = AccountPage; // Export the non-connected version for ease in unit testing
export default EnhancedComponent; // connect(mapStateToProps)(AccountListView);
@@ -0,0 +1,26 @@
import { shallow } from "enzyme";
import React from "react";
import AsyncState from "../../../shared/enumerations/AsyncState";
import { _AccountPageTabView } from "./AccountPageTabView";
{/* - 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("AccountPageTabView Container", () => {
it ("Only Renders Partial Tab if SelectedAccount is Undefined", () => {
const wrapper = shallow(
<_AccountPageTabView
repoCompanies={{ state: AsyncState.Inactive}}
putCollateral={() => {}}
markRetitled={() => {}}
externalUserMap={[]}
/>
);
const emptyTabs = wrapper.find(`[data-test='account-page-tab-view-empty-tabs']`);
const loadedTabs = wrapper.find(`[data-test='account-page-tab-view-loaded-tabs']`);
expect(emptyTabs.length).toBe(1);
expect(loadedTabs.length).toBe(0);
});
});
*/}
@@ -0,0 +1,333 @@
/* eslint-disable eqeqeq */
import Axios from "axios";
import moment from "moment/moment";
import React from "react";
import {connect} from "react-redux";
import {toast} from "react-toastify";
import TabView from "../../../components/tab-view/TabView";
import {IRepoCompany} from "../../../reducers/RepoCompanyReducer";
import {IExternalUser} from "../../../reducers/SettingsReducer";
import {HistorySource, ITemenosHistory} from "../../../services/service-types/AccountHistoryTypes";
import {IRepoAccount} from "../../../services/service-types/RepoAccountTypes";
import Configuration from "../../../shared/configuration/Configuration";
import DateConversions from "../../../shared/conversions/DateConversions";
import AsyncState from "../../../shared/enumerations/AsyncState";
import AxiosMessageFormatting from "../../../shared/formatting/AxiosMessageFormatting";
import UrlFormatting from "../../../shared/formatting/UrlFormatting";
import ToastDefaults from "../../../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../../store/Store";
import AccountActivity from "../../account-activity/AccountActivity";
import AuctionTab from "../../auction-tab/AuctionTab";
import RefundStatusTab from "../../refund-status-tab/RefundStatusTab";
import RepoHistoryTab from "../../repo-history-tab/RepoHistoryTab";
import RepoInfoTab from "../../repo-info-tab/RepoInfoTab";
import RepoStatusChangeHistoryTable from "../../repo-status-change-history-table/RepoStatusChangeHistoryTable";
import TransactionHistoryTable, {ITransactionHistory} from "../../transaction-history-table/TransactionHistoryTable";
import {getExternalUserMap, getRepoTypeMap} from "./../../../selectors/SettingsSelector";
interface IAccountPageTabViewProps {
selectedAccount?: IRepoAccount;
}
interface IPropsFromState {
readonly repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
readonly externalUserMap: IExternalUser[];
readonly repoTypeMap: string[];
readonly transactionTypeCodes: string[];
readonly transactionTypeCodeQueryPrefix: string;
readonly transactionHistoryParameterDateFormat: string;
readonly transactionHistoryStartDateOffset: number;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoCompanies: state.repoCompanyState.repoCompanies,
externalUserMap: getExternalUserMap(state),
repoTypeMap: getRepoTypeMap(state),
transactionTypeCodes: state.settingsState.transactionTypeCodes,
transactionTypeCodeQueryPrefix: state.settingsState.transactionTypeCodeQueryPrefix,
transactionHistoryParameterDateFormat: state.settingsState.transactionHistoryParameterDateFormat,
transactionHistoryStartDateOffset: state.settingsState.transactionHistoryStartDateOffset
};
};
interface IAccountPageState {
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
transactionHistories: IAsyncStateContainer<ITransactionHistory[]>;
}
type Props = IAccountPageTabViewProps & IPropsFromState;
class AccountPageTabView extends React.Component<Props, IAccountPageState> {
constructor(props: Props) {
super(props);
this.state = {
caseHistories: {state: AsyncState.Inactive},
accountHistories: {state: AsyncState.Inactive},
transactionHistories: {state: AsyncState.Inactive},
};
this.updateCaseHistory = this.updateCaseHistory.bind(this);
this.updateAccountHistory = this.updateAccountHistory.bind(this);
this.updateTransactionHistory = this.updateTransactionHistory.bind(this);
this.updateHistories = this.updateHistories.bind(this);
}
componentDidUpdate(prevProps: Props) {
// Update letters if the Selected Account has updated
if (this.props.selectedAccount !== prevProps.selectedAccount) {
this.updateHistories();
}
}
async updateCaseHistory() {
if (!this.props.selectedAccount) {
return;
}
if (
this.state.caseHistories.state === AsyncState.Inactive ||
this.state.caseHistories.state === AsyncState.Failure
) {
this.setState({
caseHistories: {
state: AsyncState.Loading,
},
});
if (this.props.selectedAccount.referenceCaseId) {
const caseHistories = await this.getCaseHistory(this.props.selectedAccount.referenceCaseId);
this.setState({
caseHistories: caseHistories,
});
} else {
this.setState({
caseHistories: {
state: AsyncState.Failure
}
});
}
}
}
async updateAccountHistory() {
if (!this.props.selectedAccount) {
return;
}
if (
this.state.accountHistories.state === AsyncState.Inactive ||
this.state.accountHistories.state === AsyncState.Failure
) {
this.setState({
accountHistories: {
state: AsyncState.Loading,
},
});
if (this.props.selectedAccount.referenceId) {
const accountHistories = await this.getAccountHistory(this.props.selectedAccount.referenceId);
this.setState({
accountHistories: accountHistories,
});
} else {
this.setState({
accountHistories: {
state: AsyncState.Failure
}
});
}
}
}
async updateTransactionHistory() {
if (!this.props.selectedAccount) {
return;
}
if (
this.state.transactionHistories.state === AsyncState.Inactive ||
this.state.transactionHistories.state === AsyncState.Failure
) {
this.setState({
transactionHistories: {
state: AsyncState.Loading,
},
});
const transactionHistories = await this.getTransactionHistory(this.props.selectedAccount.accountNo);
this.setState({
transactionHistories: transactionHistories,
});
}
}
async getCaseHistory(caseId: string): Promise<IAsyncStateContainer<ITemenosHistory[]>> {
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.case_history),
{key: "caseId", value: caseId},
);
try {
const result = await Axios.get(parameterizedUrl);
const historyResult = result.data as ITemenosHistory[];
historyResult.map((x) => {
x.Created = DateConversions.utcToMoment(x.Created);
x.Source = HistorySource.Case;
return x;
});
return {
data: historyResult,
state: AsyncState.Success,
};
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure,
};
}
}
async getAccountHistory(accountNo: string): Promise<IAsyncStateContainer<ITemenosHistory[]>> {
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.account_history),
{key: "accountNo", value: accountNo},
);
try {
const result = await Axios.get(parameterizedUrl);
const historyResult = result.data as ITemenosHistory[];
historyResult.map((x) => {
x.Created = DateConversions.utcToMoment(x.Created);
x.Source = HistorySource.Account;
return x;
});
return {
data: historyResult,
state: AsyncState.Success,
};
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure,
};
}
}
async getTransactionHistory(accountNo: string): Promise<IAsyncStateContainer<ITransactionHistory[]>> {
const parameterizedUrl = this.getTransactionHistoriesURL(accountNo);
if (!parameterizedUrl) {
return {
state: AsyncState.Failure,
};
}
try {
const result = await Axios.get(parameterizedUrl);
if (result.data.length < 1)
{
return result.data;
}
const transactionHistoryResult = result.data as ITransactionHistory[];
transactionHistoryResult.map((x) => {
x.postingDate = DateConversions.utcToMoment(x.postingDate);
x.effectiveDate = DateConversions.utcToMoment(x.effectiveDate);
return x;
});
return {
data: transactionHistoryResult,
state: AsyncState.Success,
};
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure,
};
}
}
getTransactionHistoriesURL(accountNo: string): string | undefined {
return UrlFormatting.ReplaceURLParams(
Configuration.PrefixCoreUrl(Configuration.Get.urls.core.loan_transactions),
{key: "loanNo", value: accountNo},
{key: "startDate", value: moment().add(this.props.transactionHistoryStartDateOffset, "d").format(this.props.transactionHistoryParameterDateFormat)},
{key: "endDate", value: moment().format(this.props.transactionHistoryParameterDateFormat)}
) + UrlFormatting.FormatArrayValuesIntoQuery(this.props.transactionTypeCodes, this.props.transactionTypeCodeQueryPrefix);
}
updateHistories() {
this.updateCaseHistory();
this.updateAccountHistory();
}
render() {
if (!this.props.selectedAccount) {
return (
<div data-test="account-page-tab-view-empty-tabs">
<TabView
pages={[
{
header: "Activity",
element: (<div/>),
},
]}
/>
</div>
);
} else {
return (
<div data-test="account-page-tab-view-loaded-tabs">
<TabView
pages={[
{
header: "Activity",
element: <AccountActivity selectedAccount={this.props.selectedAccount}
caseHistories={this.state.caseHistories}
accountHistories={this.state.accountHistories}
externalUserMap={this.props.externalUserMap}/>,
tabOpenCallback: this.updateHistories,
},
{
header: "Repo Info",
element: <RepoInfoTab selectedAccount={this.props.selectedAccount}
repoCompanies={this.props.repoCompanies.data}
repoTypeMap={this.props.repoTypeMap}/>,
},
{
header: "Auction Info",
element: <AuctionTab selectedAccount={this.props.selectedAccount}
caseHistories={this.state.caseHistories}
accountHistories={this.state.accountHistories}/>,
tabOpenCallback: this.updateHistories,
},
{
header: "Repo History",
element: <RepoHistoryTab accountNo={this.props.selectedAccount.accountNo}
accountHistories={this.state.accountHistories}
caseHistories={this.state.caseHistories}
externalUserMap={this.props.externalUserMap}/>,
tabOpenCallback: this.updateHistories
},
{
header: "Transaction History",
element: <TransactionHistoryTable accountNo={this.props.selectedAccount.accountNo}
transactionHistories={this.state.transactionHistories}/>,
tabOpenCallback: this.updateTransactionHistory
},
{
header: "Repo Status Changes",
element: <RepoStatusChangeHistoryTable accountNo={this.props.selectedAccount.accountNo}
repoStatusChanges={this.props.selectedAccount.repoStatusChanges}/>,
},
{
header: "Refund Status",
element: <RefundStatusTab selectedAccount={this.props.selectedAccount}/>
}
]}
/>
</div>
);
}
}
}
export const _AccountPageTabView = AccountPageTabView;
export default connect<IPropsFromState, null, {}, IAppState>(mapStateToProps, null)(AccountPageTabView);
@@ -0,0 +1,216 @@
import {shallow, ShallowWrapper} from "enzyme";
import { createLocation, createMemoryHistory } from "history";
import moment from "moment";
import React from "react";
import { match } from "react-router";
import {ICollectionsAccount } from "../../reducers/CollectionsAccountReducer";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {_AdminAccountLookup} from "./AdminAccountLookup";
const searchPhrase = "123-4567";
const singleCollectionsAccount : IAsyncStateContainer<ICollectionsAccount[]> = {
data: [{
accountId : 555555,
accountIdentifier: "5",
accountNumber: "123-4567",
accountStatus: "status",
accountTypeId: 5,
activeFlag: true,
bankruptcyFlag: true,
branch: "branch",
dueDate: moment("2020-12-5"),
firstPaymentDueDate: moment("2020-11-5"),
originalLoanDate: moment("1995-12-3"),
dqAmount: 5,
cases: [{
caseId: 1,
caseNumber: "123",
caseDescription: "This is a test",
caseSTageId: 5,
repoStatus: 2,
caseStatusId: 3,
caseTitle: "Test",
caseTypeBasisId: 4,
caseTypeId: 6,
createDate: moment("2020-10-5"),
createdUserId: 12345,
}]
}],
state: AsyncState.Success
};
const associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]> = {
data : [ {repoId: 5,
accountNo: "4567",
status: "04R",
memberNo: 123456,
loanNo: 164,
accountTypeCode: "typecode",
branch: 12,
balance: 100.00,
delinquentAmount: 150,
daysDelinquent: 2,
requiredPaymentAmount: 105.00,
origAmount: 1,
primaryIndividualId: "indvid",
borrowers: [],
collaterals: [],
repoStatusChanges: [],
origAccountTypeCode: "test"
}],
state: AsyncState.Success
};
const didAddToQueue: IAsyncStateContainer<boolean> = {
data: false,
state: AsyncState.Inactive
};
const history = createMemoryHistory();
const path = `/route/:id`;
const routematch: match<{ id: string }> = {
isExact: false,
path,
url: path.replace(":id", "1"),
params: { id: "1" }
};
const location = createLocation(routematch.url);
describe("AdminAccountLookup Container OK", () => {
let wrapper: ShallowWrapper;
const combinedStates =
beforeEach(() => {
wrapper = shallow(<_AdminAccountLookup
didAddToQueue={didAddToQueue}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>
);
});
it("Renders The Main Container", () => {
const component = wrapper.find(`[data-test='account-lookup-main']`);
expect(component.length).toBe(1);
});
it("Renders AdminAccountLookupDetails", () =>{
const component = wrapper.find(`[data-test='collections-details']`);
expect(component.length).toBe(1);
});
it("Renders Button For Pushing to Repo Account", () => {
const component = wrapper.find(`[data-test='put-to-queue-button-found']`);
expect(component.length).toBe(1);
});
it("Renders Put Notification Success",() => {
const tempDidAddToQueue = didAddToQueue;
tempDidAddToQueue.data = true;
tempDidAddToQueue.state = AsyncState.Success;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={tempDidAddToQueue}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>
);
const notification = tempWrapper.find(`[data-test='add-to-queue-success']`);
expect(notification.length).toBe(1);
const button = tempWrapper.find(`[data-test='put-to-queue-button-found']`);
expect(button.length).toBe(1);
});
it("Does Not Render Active Put Button When Collections Is Not Found",() => {
const noCollectionsAccount = singleCollectionsAccount;
noCollectionsAccount.data = [];
noCollectionsAccount.state = AsyncState.Failure;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={didAddToQueue}
collectionsAccount={noCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const button = tempWrapper.find(`[data-test='put-to-queue-button-not-found']`);
expect(button.length).toBe(1);
});
it("Renders Loading Put To Queue", () => {
const loadingPut = didAddToQueue;
loadingPut.data = true;
loadingPut.state = AsyncState.Loading;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={loadingPut}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const notification = tempWrapper.find(`[data-test='add-to-queue-loading']`);
expect(notification.length).toBe(1);
});
it("Renders Success Put To Queue", () => {
const loadingPut = didAddToQueue;
loadingPut.data = true;
loadingPut.state = AsyncState.Success;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={loadingPut}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const notification = tempWrapper.find(`[data-test='add-to-queue-success']`);
expect(notification.length).toBe(1);
});
it("Renders Failed Put To Queue", () => {
const loadingPut = didAddToQueue;
loadingPut.data = true;
loadingPut.state = AsyncState.Failure;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={loadingPut}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const notification = tempWrapper.find(`[data-test='add-to-queue-error']`);
expect(notification.length).toBe(1);
});
});
@@ -0,0 +1,326 @@
import {
getCollectionsAccount,
putAccountOnRepoQueue,
setSearchPhraseCollectionsAccounts
} from "../../actions/CollectionsAccountActions";
import NotificationBar, {NotificationBarType} from "../../components/notification-bar/NotificationBar";
import Axios from "axios";
import React from "react";
import {connect} from "react-redux";
import {RouteComponentProps} from "react-router";
import {toast} from "react-toastify";
import TabView from "../../components/tab-view/TabView";
import {ICollectionsAccount} from "../../reducers/CollectionsAccountReducer";
import {IRepoAccount} from "../../services/service-types/RepoAccountTypes";
import Configuration from "../../shared/configuration/Configuration";
import AsyncState from "../../shared/enumerations/AsyncState";
import StringFormatting from "../../shared/formatting/StringFormatting";
import Routes from "../../shared/Routes";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../store/Store";
import ProcessingReceiptService from "./../../services/ProccessingReceiptService";
import {IProcessingReceipt} from "./../../services/service-types/ProcessingReceiptTypes";
import DateConversions from "./../../shared/conversions/DateConversions";
import AxiosMessageFormatting from "./../../shared/formatting/AxiosMessageFormatting";
import UrlFormatting from "./../../shared/formatting/UrlFormatting";
import ToastDefaults from "./../../shared/notifications/ToastDefaults";
import {ProcessingReceiptTable} from "./../processing-receipt-table/processingReceiptTable";
import AdminAccountLookupDetails from "./admin-account-lookup-details/AdminAccountLookupDetails";
import {AdminAccountLookupReposTab} from "./admin-account-lookup-repos-tab/AdminAccountLookupReposTab";
interface IPropsFromState {
searchPhrase: string;
collectionsAccount: IAsyncStateContainer<ICollectionsAccount[]>;
didAddToQueue: IAsyncStateContainer<boolean>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
collectionsAccount: state.collectionsAccountState.collectionsAccount,
searchPhrase: state.collectionsAccountState.searchPhrase,
didAddToQueue: state.collectionsAccountState.didAddToQueue,
};
};
interface IPropsFromDispatch {
getCollectionsAccount: (accountNo: string) => any;
setSearchPhraseCollectionsAccounts: (searchPhrase: string) => any;
putAccountOnRepoQueue: (collectionsAccount: ICollectionsAccount) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
getCollectionsAccount: getCollectionsAccount,
setSearchPhraseCollectionsAccounts: setSearchPhraseCollectionsAccounts,
putAccountOnRepoQueue: putAccountOnRepoQueue
};
interface IAdminAccountLookupState {
processingReceipts: IAsyncStateContainer<IProcessingReceipt[]>;
associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]>;
}
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps;
class AdminAccountLookup extends React.Component <Props, IAdminAccountLookupState> {
constructor(props: any) {
super(props);
this.state = {
processingReceipts: {
state: AsyncState.Inactive
},
associatedRepoAccounts: {
state: AsyncState.Inactive
}
};
this.handleSearchChange = this.handleSearchChange.bind(this);
this.searchAccounts = this.searchAccounts.bind(this);
this.handleSearchKeyDown = this.handleSearchKeyDown.bind(this);
this.goToAccount = this.goToAccount.bind(this);
this.pushToQueue = this.pushToQueue.bind(this);
this.updateProcessingReceipts = this.updateProcessingReceipts.bind(this);
this.getProcessingReceipts = this.getProcessingReceipts.bind(this);
this.getAssociatedRepoAccounts = this.getAssociatedRepoAccounts.bind(this);
this.updateProcessingReceipts = this.updateProcessingReceipts.bind(this);
}
goToAccount = (account: IRepoAccount) => {
this.props.history.push(Routes.Account(account.repoId));
};
pushToQueue = () => {
if (this.props.collectionsAccount && this.props.collectionsAccount.data && this.props.collectionsAccount.data.length === 1) {
this.props.putAccountOnRepoQueue(this.props.collectionsAccount.data[0]);
}
};
async searchAccounts() {
if (this.props.searchPhrase.trim() !== "") {
await this.props.getCollectionsAccount(this.props.searchPhrase);
await this.updateAssociatedRepoAccounts();
await this.updateProcessingReceipts();
} else {
this.props.collectionsAccount.state = AsyncState.Inactive;
this.state.associatedRepoAccounts.state = AsyncState.Inactive;
}
}
async handleSearchKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.keyCode === 13) { // Enter
await this.searchAccounts();
}
}
async getAssociatedRepoAccounts(accountNo: string): Promise<IAsyncStateContainer<IRepoAccount[]>> {
const paramterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single_by_account_no), {
key: "accountNo",
value: accountNo
}
);
try {
const results = await Axios.get(paramterizedUrl);
const repoAccounts = results.data as IRepoAccount[];
return {
data: repoAccounts,
state: AsyncState.Success
};
} catch (err) {
if (err.response.status === 404) {
return {
state: AsyncState.Success,
data: []
};
}
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure
};
}
}
async getProcessingReceipts(accountNo: string): Promise<IAsyncStateContainer<IProcessingReceipt[]>> {
const paramterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.account_processing_receipts), {
key: "accountNo",
value: accountNo
}
);
try {
const results = await Axios.get(paramterizedUrl);
const processingReceiptResults = results.data as IProcessingReceipt[];
processingReceiptResults.map(x => {
x.processed = DateConversions.utcToMoment(x.processed);
return x;
});
return {
data: ProcessingReceiptService.sortByProcessedDate(processingReceiptResults),
state: AsyncState.Success
};
} catch (err) {
if (err.response.status === 404) {
return {
state: AsyncState.Success,
data: []
};
}
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure
};
}
}
async updateProcessingReceipts() {
this.setState({
processingReceipts: {
state: AsyncState.Loading
}
});
const processingReceipts = await this.getProcessingReceipts(this.props.searchPhrase);
this.setState({
processingReceipts: processingReceipts
});
}
async updateAssociatedRepoAccounts() {
this.setState({
associatedRepoAccounts: {
state: AsyncState.Loading
}
});
const associatedRepoAccounts = await this.getAssociatedRepoAccounts(this.props.searchPhrase);
this.setState({
associatedRepoAccounts: associatedRepoAccounts
});
}
handleSearchChange(event: React.ChangeEvent<HTMLInputElement>) {
this.props.setSearchPhraseCollectionsAccounts(StringFormatting.TrimLeft(event.target.value));
}
render() {
return (
<div className="container-fluid main-panel" data-test="account-lookup-main">
{createPutToQueueNotifications(this.props.didAddToQueue.state)}
<div>
<h2>Account Lookup</h2>
<div className="form-inline d-flex">
<input
data-test="text-input"
className="form-control mr-sm-2 mb-3"
type="search"
aria-label="Search"
placeholder="Account No"
value={this.props.searchPhrase}
onChange={this.handleSearchChange}
onKeyDown={this.handleSearchKeyDown}
/>
<button
className="btn btn-primary mb-3"
type="submit"
onClick={this.searchAccounts}
>
Search
</button>
{createPushToRepoButton(this.props.collectionsAccount.state, this.pushToQueue, this.props.didAddToQueue.data)}
</div>
<hr/>
<AdminAccountLookupDetails data-test="collections-details"
thisAccount={this.props.collectionsAccount}/>
<hr/>
</div>
<TabView
pages={[
{
header: "Associated Repo Accounts",
element: <AdminAccountLookupReposTab goToAccount={this.goToAccount}
associatedRepoAccounts={this.state.associatedRepoAccounts}/>
},
{
header: "Processing Receipt History",
element: <ProcessingReceiptTable processingReceipts={this.state.processingReceipts}/>,
}]}
/>
</div>
);
}
}
const createPushToRepoButton = (collectionsState: AsyncState, onClickAction: Function, didAddToQueue: boolean | undefined) => {
if (collectionsState === AsyncState.Success) {
return (
<button
data-test="put-to-queue-button-found"
className="btn btn-primary mb-3 ml-auto"
onClick={() => {
onClickAction();
}}
disabled={didAddToQueue}
>
Push To Repo Queue
</button>
);
} else {
return (
<button
data-test="put-to-queue-button-not-found"
className="btn btn-primary mb-3 ml-auto"
disabled={true}
>
Push To Repo Queue
</button>
);
}
};
const createPutToQueueNotifications = (addToQueueState: AsyncState) => {
if (addToQueueState === AsyncState.Loading) {
return (
<div className="mt-2 mb-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Loading}
text={`Adding To Repo Queue`}
data-test="add-to-queue-loading"
/>
</div>
);
}
if (addToQueueState === AsyncState.Success) {
return (
<div className="mt-2 mb-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Info}
text={`Added Account To RepoQueue`}
data-test="add-to-queue-success"
/>
</div>
);
}
if (addToQueueState === AsyncState.Failure) {
return (
<div className="mt-2 mb-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Danger}
text={`Failed To Add Account To Repo Queue`}
data-test="add-to-queue-error"
/>
</div>
);
}
};
export const _AdminAccountLookup = AdminAccountLookup;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminAccountLookup);
@@ -0,0 +1,12 @@
.collections-page-input-section p, .collections-page-input-section input {
font-size: .91em;
}
.collections-page-input-section input {
height: 2em;
}
.placeholder{
width: 80%;
height: 20vh;
}
@@ -0,0 +1,157 @@
import React from "react";
import moment from "moment";
import {shallow, ShallowWrapper} from 'enzyme';
import AsyncState from "../../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import {ICollectionsAccount} from "../../../reducers/CollectionsAccountReducer"
import AdminAccountLookupDetails from './AdminAccountLookupDetails';
describe('AdminAccountLookupDetails OK', () => {
let wrapper: ShallowWrapper;
const singleCollectionsAccount : IAsyncStateContainer<ICollectionsAccount[]> = {
data: [{
accountId : 555555,
accountIdentifier: "5",
accountNumber: "123-4567",
accountStatus: "status",
accountTypeId: 5,
activeFlag: true,
bankruptcyFlag: true,
branch: "branch",
dueDate: moment("2020-12-5"),
firstPaymentDueDate: moment("2020-11-5"),
originalLoanDate: moment("1995-12-3"),
dqAmount: 5,
cases: [{
caseId: 1,
caseNumber: "123",
caseDescription: "This is a test",
caseSTageId: 5,
repoStatus: 2,
caseStatusId: 3,
caseTitle: "Test",
caseTypeBasisId: 4,
caseTypeId: 6,
createDate: moment("2020-10-5"),
createdUserId: 12345,
}]
}],
state: AsyncState.Success
};
var combinedStates =
beforeEach(() => {
wrapper = shallow(<AdminAccountLookupDetails thisAccount = {singleCollectionsAccount}/>);
})
it('Lookup Details Ok', () => {
const CollectionsAccountDetails = wrapper.find(`[data-test='Collections-Display']`);
const CollectionsCaseDisplay = wrapper.find(`[data-test='Case-Display']`);
expect(CollectionsAccountDetails.length).toBe(1);
expect(CollectionsCaseDisplay.length).toBe(1);
});
it('Lookup Details Undefined Data', () => {
const undefinedData : IAsyncStateContainer<ICollectionsAccount[]> = {
data: undefined,
state: AsyncState.Success
}
var tempWrapper = shallow(<AdminAccountLookupDetails thisAccount = {undefinedData} />);
const CollectionsAccountDetails = tempWrapper.find(`[data-test='Collections-Display']`);
const CollectionsCaseDisplay = tempWrapper.find(`[data-test='Case-Display-Empty']`);
expect(CollectionsAccountDetails.length).toBe(0);
expect(CollectionsCaseDisplay.length).toBe(1);
});
it('Produces Loading Collections Notification', () => {
const loadingCollectionsAccount : IAsyncStateContainer<ICollectionsAccount[]> = {
data: [],
state: AsyncState.Loading
}
var tempWrapper = shallow(<AdminAccountLookupDetails thisAccount = {loadingCollectionsAccount} />);
const notification = tempWrapper.find(`[data-test='loading-alert']`)
expect(notification.length).toBe(1);
});
it('Produces Failure Collections Notification When Account Is Undefined', () => {
const loadingCollectionsAccount : IAsyncStateContainer<ICollectionsAccount[]> = {
data: [],
state: AsyncState.Loading
}
var tempWrapper = shallow(<AdminAccountLookupDetails thisAccount = {loadingCollectionsAccount} />);
const notification = tempWrapper.find(`[data-test='loading-alert']`)
expect(notification.length).toBe(1);
});
it('Produces Failure Notification When Multiple Accounts Are Found', () => {
const multipleCollectionsAccounts : IAsyncStateContainer<ICollectionsAccount[]> = {
data: [{
accountId : 555555,
accountIdentifier: "5",
accountNumber: "123-4567",
accountStatus: "status",
accountTypeId: 5,
activeFlag: true,
bankruptcyFlag: true,
branch: "branch",
dueDate: moment("2020-12-5"),
firstPaymentDueDate: moment("2020-11-5"),
originalLoanDate: moment("1995-12-3"),
dqAmount: 5,
cases: [{
caseId: 1,
caseNumber: "123",
caseDescription: "This is a test",
caseSTageId: 5,
repoStatus: 2,
caseStatusId: 3,
caseTitle: "Test",
caseTypeBasisId: 4,
caseTypeId: 6,
createDate: moment("2020-10-5"),
createdUserId: 12345,
}]
}, {
accountId : 555555,
accountIdentifier: "5",
accountNumber: "123-4567",
accountStatus: "status",
accountTypeId: 5,
activeFlag: true,
bankruptcyFlag: true,
branch: "branch",
dueDate: moment("2020-12-5"),
firstPaymentDueDate: moment("2020-11-5"),
originalLoanDate: moment("1995-12-3"),
dqAmount: 5,
cases: [{
caseId: 1,
caseNumber: "123",
caseDescription: "This is a test",
caseSTageId: 5,
repoStatus: 2,
caseStatusId: 3,
caseTitle: "Test",
caseTypeBasisId: 4,
caseTypeId: 6,
createDate: moment("2020-10-5"),
createdUserId: 12345,
}]
}],
state: AsyncState.Success
}
var tempWrapper = shallow(<AdminAccountLookupDetails thisAccount = {multipleCollectionsAccounts} />);
const notification = tempWrapper.find(`[data-test='fatal-alert']`)
expect(notification.length).toBe(1);
const mutipleDisplays = tempWrapper.find(`[data-test='Collections-Display']`)
expect(mutipleDisplays.length).toBe(2);
});
});
@@ -0,0 +1,73 @@
import React from "react";
import NotificationBar, { NotificationBarType } from "../../../components/notification-bar/NotificationBar";
import {ICollectionsAccount} from "../../../reducers/CollectionsAccountReducer";
import AsyncState from "../../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import CollectionsAccountDisplay, {CreateCollectionsAccountDisplayPlaceholder} from "../collections-account-display/CollectionsAccountDisplay";
import CollectionsCaseDisplay from "../collections-case-display/CollectionsCaseDisplay";
import "./AdminAccountLookupDetails.css";
export interface ICollectionsAccountDetailsProps{
thisAccount: IAsyncStateContainer<ICollectionsAccount[]>;
}
const CollectionsAccountDetails: React.FC<ICollectionsAccountDetailsProps> = (props) => {
const ret: any[] = [];
if(props.thisAccount===undefined || props.thisAccount.state === AsyncState.Failure){
ret.push(
<NotificationBar
key="alert-notification"
type={NotificationBarType.Warning}
text={`Could Not Find A Collections Account With This Account Number`}
data-test="error-alert"
/>
);
}
else if(props.thisAccount.state === AsyncState.Loading){
ret.push(
<NotificationBar
key="alert-notification"
type={NotificationBarType.Loading}
text={`Loading Collections Account Information`}
data-test="loading-alert"
/>
);
}else if(props.thisAccount.data !== undefined && props.thisAccount.data.length > 1){
ret.push(
<NotificationBar
key="alert-notification"
type={NotificationBarType.Danger}
text={`This Collections Account No is associated with multiple accounts`}
data-test="fatal-alert"
/>
);
}
if(props.thisAccount.data!==undefined){
ret.push(
<div className="container" key="account-info1">
<h5>Collections Account Information</h5>
{props.thisAccount.data.map((d,x) => {
return(
<div key={x}>
<CollectionsAccountDisplay data-test="Collections-Display" CollectionsAccount={d}/>
<CollectionsCaseDisplay data-test="Case-Display" CollectionsCases={d.cases}/>
</div>
);
})}
</div>
);
}else{
ret.push(
<div className="container" key="account-info1">
{CreateCollectionsAccountDisplayPlaceholder()}
<CollectionsCaseDisplay data-test="Case-Display-Empty" CollectionsCases={[]}/>
</div>
);
}
return (
<div>{ret}</div>
);
};
export default CollectionsAccountDetails;
@@ -0,0 +1,24 @@
import React from "react";
import {AccountListTable} from "../../../containers/account-list-view/account-list-table/AccountListTable";
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import AsyncContainer from "../../async-container/AsyncContainer";
export interface IAdminAccountLookupReposTab {
goToAccount: (repoAccount: IRepoAccount) => any;
associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]>;
}
export const AdminAccountLookupReposTab: React.FC<IAdminAccountLookupReposTab> = (props) => {
return(
<div>
<AsyncContainer container={props.associatedRepoAccounts} message={"Loading related Repo accounts."} data-test="loading-repo-accounts-notifications">
<AccountListTable
data-test="associated-repos"
repoAccounts={props.associatedRepoAccounts.data !== undefined ? props.associatedRepoAccounts.data : []}
onRepoAccountClicked={props.goToAccount}
/>
</AsyncContainer>
</div>
)
}
@@ -0,0 +1,79 @@
import moment from "moment";
import React from "react";
import {ICollectionsAccount} from "../../../reducers/CollectionsAccountReducer";
import "../admin-account-lookup-details/AdminAccountLookupDetails.css";
export interface ICollectionsAccountDisplay{
CollectionsAccount: ICollectionsAccount;
}
const CollectionsAccountDisplay: React.FC<ICollectionsAccountDisplay> = (props) => {
return(
<div data-test="collections-done" className="collections-page-input-section row">
<div className="col-sm">
<p className="mb-0">Account ID</p>
<input data-test="collections-page-AccountId" className="form-control mb-1" value={props.CollectionsAccount.accountId} disabled={true} />
<p className="mb-0">Account Identifier</p>
<input data-test="collections-page-AccountIdentifier" className="form-control mb-1" value={props.CollectionsAccount.accountIdentifier} disabled={true} />
<p className="mb-0">Acccount Number</p>
<input data-test="collections-page-AccountNumber" className="form-control mb-1" value={props.CollectionsAccount.accountNumber} disabled={true} />
<p className="mb-0">Acccount Status</p>
<input data-test="collections-page-AccountStatus" className="form-control mb-1" value={props.CollectionsAccount.accountStatus} disabled={true} />
</div>
<div className="col-sm">
<p className="mb-0">Active Flag</p>
<input data-test="collections-page-ActiveFlag" className="form-control mb-1" value={props.CollectionsAccount.activeFlag? "true" : "false"} disabled={true} />
<p className="mb-0">Bankrupcy Flag</p>
<input data-test="collections-page-BankruptcyFlag" className="form-control mb-1" value={props.CollectionsAccount.bankruptcyFlag? "true" : "false"} disabled={true} />
<p className="mb-0">Branch</p>
<input data-test="collections-page-Branch" className="form-control mb-1" value={props.CollectionsAccount.branch} disabled={true} />
</div>
<div className="col-sm">
<p className="mb-0">First Payment Due Date</p>
<input data-test="collections-page-FirstPaymentDueDate" className="form-control mb-1"value={moment(props.CollectionsAccount.firstPaymentDueDate).format("MM-DD-YYYY")} disabled={true} />
<p className="mb-0">Original Loan Date</p>
<input data-test="collections-page-OriginalLoanDate" className="form-control mb-1" value={moment(props.CollectionsAccount.originalLoanDate).format("MM-DD-YYYY")} disabled={true} />
<p className="mb-0">Due Date</p>
<input data-test="collections-page-DueDate" className="form-control mb-1" value={moment(props.CollectionsAccount.dueDate).format("MM-DD-YYYY")} disabled={true} />
</div>
</div>
);
};
export function CreateCollectionsAccountDisplayPlaceholder() : any {
return(
<div>
<h5>Collections Account Information</h5>
<div data-test="collections-placeholder" className="collections-page-input-section row">
<div className="col-sm">
<p className="mb-0">Account ID</p>
<input data-test="collections-page-AccountId" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Account Identifier</p>
<input data-test="collections-page-AccountIdentifier" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Acccount Number</p>
<input data-test="collections-page-AccountNumber" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Acccount Status</p>
<input data-test="collections-page-AccountStatus" className="form-control mb-1" value="-" disabled={true} />
</div>
<div className="col-sm">
<p className="mb-0">Active Flag</p>
<input data-test="collections-page-ActiveFlag" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Bankrupcy Flag</p>
<input data-test="collections-page-BankruptcyFlag" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Branch</p>
<input data-test="collections-page-Branch" className="form-control mb-1" value="-" disabled={true} />
</div>
<div className="col-sm">
<p className="mb-0">First Payment Due Date</p>
<input data-test="collections-page-FirstPaymentDueDate" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Original Loan Date</p>
<input data-test="collections-page-OriginalLonDate" className="form-control mb-1" value="-" disabled={true} />
<p className="mb-0">Due Date</p>
<input data-test="collections-page-DueDate" className="form-control mb-1" value="-" disabled={true} />
</div>
</div>
</div>
);
}
export default CollectionsAccountDisplay;
@@ -0,0 +1,33 @@
import React from "react";
import {ICollectionsCase} from "../../../reducers/CollectionsAccountReducer";
export interface ICollectionsCaseDisplay{
CollectionsCases: ICollectionsCase[];
}
const CollectionsCaseDisplay: React.FC<ICollectionsCaseDisplay> = (props) => {
return(
<div className="row px-3">
<p className="mb-0">Cases</p>
<table className="table table-striped table-hover">
<thead className="thead-dark">
<tr>
<th className="py-0">CaseID</th>
<th className="py-0">Title</th>
<th className="py-0">Description</th>
</tr>
</thead>
<tbody>
{props.CollectionsCases.map((c)=>{return(
<tr key={c.caseId} data-test="table-row">
<td>{c.caseId}</td>
<td>{c.caseTitle}</td>
<td>{c.caseDescription}</td>
</tr>
);})}
</tbody>
</table>
</div>
);
};
export default CollectionsCaseDisplay;
@@ -0,0 +1,72 @@
import { shallow, ShallowWrapper } from "enzyme";
import { CustomAsyncNotification } from "lcu-react-lib";
import { wrap } from "module";
import React from "react";
import { IAuctionAssignment } from "../../reducers/AuctionAssignmentReducer";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { _AdminAuctionHouses } from "./AdminAuctionHouses";
describe("AdminAuctionHouses Container", () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<_AdminAuctionHouses
auctionHouses={
{
state: AsyncState.Success,
data:[] //Empty Array counts as "having data"
}
}
postedAuctionHouse={
{
state: AsyncState.Loading
}
}
loadAuctionHouses={()=>{}}
addNewAuctionHouse={(auctionHouse : IAuctionHouse)=>{}}
updateAuctionHouse={(auctionHouse : IAuctionHouse)=>{}}
/>
);
});
it("Renders All Components When Data is Present", () => {
expect(wrapper.find(`[data-test='admin-auction-houses-async-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='posted-auction-house-async-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='admin-auction-houses-async-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='add-auction-house-button']`).length).toBe(1);
expect(wrapper.find(`[data-test='admin-auction-house-form']`).length).toBe(1);
expect(wrapper.find(`[data-test='admin-auction-houses-table']`).length).toBe(1);
});
it("Creates Success Notification", () => {
let customNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='posted-auction-house-async-notification']`).prop("customNotifications");
let container: IAsyncStateContainer<IAuctionHouse> = wrapper.find(`[data-test='posted-auction-house-async-notification']`).prop("container");
expect(customNotification[0].displayWhen(container.data)).toBe(false);
wrapper.setProps({
postedAuctionHouse:
{
state: AsyncState.Success,
data: []
}
});
wrapper.update();
customNotification = wrapper.find(`[data-test='posted-auction-house-async-notification']`).prop("customNotifications");
container = wrapper.find(`[data-test='posted-auction-house-async-notification']`).prop("container");
expect(customNotification[0].displayWhen(container)).toBe(true);
});
it("Does Not Render Form Or Table and Disables Option to add new When Data Is Not Present", () => {
wrapper.setProps({
auctionHouses: {
state: AsyncState.Failure,
}
});
expect(wrapper.find(`[data-test='add-auction-house-button']`).prop("disabled")).toBe(true);
expect(wrapper.find(`[data-test='admin-auction-house-form']`).length).toBe(0);
expect(wrapper.find(`[data-test='admin-auction-houses-table']`).length).toBe(0);
});
});
@@ -0,0 +1,141 @@
import { AsyncNotification, NotificationType } from "lcu-react-lib";
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { getAllAuctionHouses, postAuctionHouse, putAuctionHouse } 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";
import AdminAuctionHouseFormContainer from "./admin-auction-house-form-container/AdminAuctionHouseFormContainer";
import AdminAuctionHousesTable from "./admin-auction-houses-table/AdminAuctionHousesTable";
interface IPropsFromState {
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
postedAuctionHouse: IAsyncStateContainer<IAuctionHouse>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
auctionHouses: state.auctionHouseState.auctionHouses,
postedAuctionHouse: state.auctionHouseState.postedAuctionHouse
};
};
interface IPropsFromDispatch {
loadAuctionHouses: () => any;
addNewAuctionHouse: (auctionHouse: IAuctionHouse) => any;
updateAuctionHouse: (auctionHouse: IAuctionHouse) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
loadAuctionHouses: getAllAuctionHouses,
addNewAuctionHouse: postAuctionHouse,
updateAuctionHouse: putAuctionHouse
};
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps;
const AdminAuctionHouses: React.FC<Props> = (props) => {
const [formCollapsed, setFormCollapsed] = useState<boolean>(true);
const [selectedAccount, setSelectedAccount] = useState<IAuctionHouse | undefined>(undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (props.auctionHouses.state === AsyncState.Inactive) {
props.loadAuctionHouses();
setFormCollapsed(true);
setSelectedAccount(undefined);
}
});
return (
<div className="container-fluid main-panel" data-test="vehicle-floor-pricing-parameters">
<AsyncNotification
container={props.auctionHouses}
subject={"Auction Houses"}
data-test="admin-auction-houses-async-notification"
/>
<AsyncNotification
container={props.postedAuctionHouse}
subject={". Auction House not Posted"}
customNotifications={
[
{
displayWhen: (data) => {return data? true : false;},
message: "Succsessfully Sent Auction House",
type: NotificationType.Success
}
]
}
data-test="posted-auction-house-async-notification"
/>
<div className="row">
<h2 className="align-self-right col mb-2">Auction House Manager </h2>
<div className="d-flex flex-row-reverse col mb-2">
{(formCollapsed)? (
<button
className="btn btn-primary align-self-center"
disabled={!props.auctionHouses.data}
onClick={() => {
setSelectedAccount(undefined);
setFormCollapsed(false);
}
}
data-test="add-auction-house-button"
>
Add New Auction House
</button>
) : (
<button
className="btn btn-primary align-self-center btn-danger"
disabled={!props.auctionHouses.data}
onClick={() => {
setFormCollapsed(true);
setSelectedAccount(undefined);
}}
data-test="cancel-add-auction-house"
>
Abandon Changes
</button>
)
}
</div>
</div>
{
(
<div>
{props.auctionHouses.data?
(
<div>
<AdminAuctionHouseFormContainer
newAuctionHouseSubmit={(newHouse: IAuctionHouse)=>{props.addNewAuctionHouse(newHouse);}}
updateAuctionHouseSubmit={((houseToUpdate: IAuctionHouse)=>{props.updateAuctionHouse(houseToUpdate);})}
clearSelectedAuction={() => setSelectedAccount(undefined)}
auctionHouses={props.auctionHouses.data}
isCollapsed={formCollapsed}
postedAuctionHouse={props.postedAuctionHouse}
auctionHouseToUpdate={selectedAccount}
data-test="admin-auction-house-form"
/>
<AdminAuctionHousesTable
auctionHouses={props.auctionHouses.data}
selectForEdit={(auctionHouse: IAuctionHouse)=>{
setSelectedAccount(auctionHouse);
setFormCollapsed(false);
window.scrollTo(0, 0);
}}
data-test="admin-auction-houses-table"
/>
</div>
): null}
</div>
)
}
</div>
);
};
export const _AdminAuctionHouses = AdminAuctionHouses;
export default withRouter(connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminAuctionHouses));
@@ -0,0 +1,371 @@
import { shallow } from "enzyme";
import React from "react";
import AsyncState from "../../../shared/enumerations/AsyncState";
import AdminAuctionHouseFormContainer from "./AdminAuctionHouseFormContainer";
describe("AdminAuctionHouses Container", () => {
let wrapper: any;
beforeEach(()=>{
wrapper = shallow(
<AdminAuctionHouseFormContainer
auctionHouses={
[{
auctionID: "10101",
name: "Test",
active: "yes",
address_Line_1: "1010 1st St.",
address_Line_2: "",
city: "New York",
state: "NY",
zip: 51112,
country: "USA",
phone: "14141414",
fax: "14141414",
}]
}
postedAuctionHouse={{
state: AsyncState.Inactive
}}
isCollapsed={true}
newAuctionHouseSubmit={()=>{}}
clearSelectedAuction={()=>{}}
updateAuctionHouseSubmit={()=>{}}
auctionHouseToUpdate={undefined}
/>
);
});
it("Shows the post form when not collapsed and no auction to update", () => {
wrapper.setProps({
isCollapsed: false
})
wrapper.update();
const form = wrapper.find(`[data-test='post-form']`);
expect(form.length).toBe(1);
});
it("Shows the put form when not collapsed and has auction to update", () => {
wrapper.setProps({
isCollapsed: false,
auctionHouseToUpdate:{
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
}
})
wrapper.update();
const form = wrapper.find(`[data-test='put-form']`);
expect(form.length).toBe(1);
});
it("Doesnt Show Forms When Collapsed", () => {
const tempWrapper = wrapper.setProps({
isCollapsed: true
});
tempWrapper.update();
const form = tempWrapper.find(`[data-test='post-form']`);
expect(form.length).toBe(0);
});
it("Shows Validation Error For Auction ID", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "10101",
name: "",
active: "",
address_Line_1: "",
address_Line_2: "",
city: "",
state: "",
zip: -1,
country: "",
phone: "",
fax: "",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(true);
});
it("Shows Validation Error For Auction ID", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "10101",
name: "",
active: "",
address_Line_1: "",
address_Line_2: "",
city: "",
state: "",
zip: -1,
country: "",
phone: "",
fax: "",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(true);
});
it("Shows Validation Error For Auction State", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "11111",
name: "",
active: "",
address_Line_1: "",
address_Line_2: "",
city: "",
state: "WEEE",
zip: -1,
country: "",
phone: "",
fax: "",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(true);
});
it("Displays Validation Error When Form Is Incomplete", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "10101",
name: "",
active: "",
address_Line_1: "",
address_Line_2: "",
city: "",
state: "WEEE",
zip: -1,
country: "",
phone: "",
fax: "",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(true);
});
it("Displays Validation Error When Form Has Validation Errors", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "10101",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WEEE",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(true);
});
it("Does not have a validation notification when the form is complete and clears from", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(false);
expect(wrapper.state("auctionHouseDetails").auctionID).toBe("");
});
it("Displays Validation Error When Form Has Validation Errors", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "10101",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WEEE",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false
})
wrapper.setState({
showValidationError: false
});
const postFormSubmit : Function = wrapper.find(`[data-test='post-form']`).prop("onSubmit");
postFormSubmit();
expect(wrapper.state("showValidationError")).toBe(true);
});
it("Disables Update Submit if No Change Made", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false,
auctionHouseToUpdate: {
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
}
})
wrapper.setState({
showValidationError: false
});
const submitDisabled : boolean = wrapper.find(`[data-test='put-form']`).prop("disableSubmitButton");
expect(submitDisabled).toBe(true);
});
it("Enables Update Submit When Change Made", () => {
wrapper.setState({
auctionHouseDetails: {
auctionID: "0000",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
},
});
wrapper.setProps({
isCollapsed: false,
auctionHouseToUpdate: {
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
}
})
wrapper.setState({
showValidationError: false
});
const submitDisabled : boolean = wrapper.find(`[data-test='put-form']`).prop("disableSubmitButton");
expect(submitDisabled).toBe(false);
});
});
@@ -0,0 +1,192 @@
import NotificationBar, {NotificationBarType} from "../../../components/notification-bar/NotificationBar";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import AdminAuctionHouseForm from "./admin-auction-house-form/AdminAuctionHouseForm";
import React from "react";
import _ from "lodash";
const clonedeep = require("lodash.clonedeep");
interface IAdminAuctionHouseFormContainerProps{
auctionHouses: IAuctionHouse[];
newAuctionHouseSubmit: (newAuctionAssignment: IAuctionHouse) => any;
updateAuctionHouseSubmit: (updatedAuctionHouse: IAuctionHouse) => any;
clearSelectedAuction: () => any;
auctionHouseToUpdate?: IAuctionHouse;
isCollapsed: boolean;
postedAuctionHouse: IAsyncStateContainer<IAuctionHouse>;
}
interface IAdminAuctionHouseFormContainerState{
auctionHouseDetails: IAuctionHouse;
showValidationError: boolean;
isFormValid: boolean;
}
const emptyAuctionHouseDetails = {
auctionID: "",
name: "",
active: "",
address_Line_1: "",
address_Line_2: "",
city: "",
state: "",
zip: -1,
country: "",
phone: "",
fax: "",
latitude: undefined,
longitude: undefined
};
class AdminAuctionHouseFormContainer extends React.Component<IAdminAuctionHouseFormContainerProps, IAdminAuctionHouseFormContainerState>
{
constructor(props: any){
super(props);
this.state = {
auctionHouseDetails: emptyAuctionHouseDetails,
showValidationError: false,
isFormValid: false
};
}
componentDidUpdate(prevProps: IAdminAuctionHouseFormContainerProps){
if(this.props.isCollapsed === true && prevProps.isCollapsed === false){
this.clearForm();
}
if(this.props.auctionHouseToUpdate && this.props.auctionHouseToUpdate !== prevProps.auctionHouseToUpdate){
this.setState(
{
showValidationError: false,
auctionHouseDetails: clonedeep(this.props.auctionHouseToUpdate)
}
);
}
}
handleInputChange(name: string, newValue: any) {
const newAuctionHouseDetails = clonedeep(this.state.auctionHouseDetails);
newAuctionHouseDetails[name] = newValue;
this.setState({
...this.state,
auctionHouseDetails: newAuctionHouseDetails
});
}
checkFormValidity(updatedAuctionHouseDetails: IAuctionHouse) {
let formComplete: boolean = true;
const newAuctionHouseDetails = clonedeep(updatedAuctionHouseDetails);
Object.keys(this.state.auctionHouseDetails).forEach(key => {
if(key === "latitude" || key === "longitude" || key === "address_Line_2") return;
if(newAuctionHouseDetails[key] === ""){
formComplete = false;
}
});
if(newAuctionHouseDetails.zip === -1){
formComplete = false;
}
if(newAuctionHouseDetails.state.length !== 2){
formComplete = false;
}
if(!this.validateHasUniqueAuctionID(this.state.auctionHouseDetails.auctionID)){
formComplete = false;
}
return formComplete;
}
validateHasUniqueAuctionID(auctionID: string) : boolean{
const matchingAuctionHouse = this.props.auctionHouses.find(house => house.auctionID === auctionID);
if(matchingAuctionHouse !== undefined){
return false;
}
return true;
}
trySubmitNew(){
const newAuctionHouseDetails = clonedeep(this.state.auctionHouseDetails);
if(this.checkFormValidity(newAuctionHouseDetails)){
this.props.newAuctionHouseSubmit(newAuctionHouseDetails);
this.clearForm();
}else{
this.setState({
showValidationError: true
});
}
}
trySubmitUpdate(){
const newAuctionHouseDetails = clonedeep(this.state.auctionHouseDetails);
if(this.checkFormValidity(newAuctionHouseDetails)){
this.props.updateAuctionHouseSubmit(newAuctionHouseDetails);
this.clearForm();
}else{
this.setState({
showValidationError: true
});
}
}
clearForm(){
this.setState({
auctionHouseDetails: emptyAuctionHouseDetails,
showValidationError: false
});
this.props.clearSelectedAuction();
}
generateValidationNotifications(){
const notifications = [];
if(this.state.showValidationError) {
notifications.push(
<NotificationBar
type={NotificationBarType.Danger}
text={"Form has Validation Errors"}
subText={"Complete the form to submit"}
key="validation"
data-test="validation-error-notification"
/>
);
}
return notifications;
}
render() {
if(!this.props.isCollapsed && !this.props.auctionHouseToUpdate){
return(
<div>
{this.generateValidationNotifications()}
<AdminAuctionHouseForm
auctionHouseDetails={this.state.auctionHouseDetails}
handleInputChange={(key: string, newValue: any) => {this.handleInputChange(key, newValue);}}
onSubmit={() => this.trySubmitNew()}
isUniqueAuctionID={this.validateHasUniqueAuctionID(this.state.auctionHouseDetails.auctionID)}
isEditing={false}
disableSubmitButton={false}
data-test={"post-form"}
/>
</div>
);
}else if(!this.props.isCollapsed && this.props.auctionHouseToUpdate){
return(
<AdminAuctionHouseForm
auctionHouseDetails={this.state.auctionHouseDetails}
handleInputChange={(key: string, newValue: any) => {this.handleInputChange(key, newValue);}}
onSubmit={() => this.trySubmitUpdate()}
isUniqueAuctionID={true}
isEditing={true}
disableSubmitButton={_.isEqual(this.state.auctionHouseDetails, this.props.auctionHouseToUpdate)}
data-test={"put-form"}
/>
);
}else{
return(<div/>);
}
}
}
export default AdminAuctionHouseFormContainer;
@@ -0,0 +1,75 @@
import React from 'react';
import { ReactWrapper, shallow, ShallowWrapper } from 'enzyme';
import AdminAuctionHousesForm from './AdminAuctionHouseForm';
import { IAuctionHouse } from '../../../../reducers/AuctionHouseReducer';
describe('AdminAuctionHouse Form', () => {
let wrapper: ShallowWrapper;
var testInput: { key: any; newValue: any; };
beforeEach(()=>{
testInput = {key: undefined, newValue: undefined}
wrapper = shallow(
<AdminAuctionHousesForm
auctionHouseDetails={
{
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
}
}
handleInputChange={(inputKey: string, newValue: any) => {testInput.key = inputKey; testInput.newValue = newValue;}}
onSubmit={()=>{}}
isUniqueAuctionID={true}
isEditing={true}
disableSubmitButton={true}
/>
);
});
it("disables submit button when button is disabled", () => {
expect(wrapper.find(`[data-test='submit-auction-house']`).prop("disabled")).toBe(true);
wrapper.setProps({
disableSubmitButton: false
})
expect(wrapper.find(`[data-test='submit-auction-house']`).prop("disabled")).toBe(false);
});
it("disables option to update AuctionID and IsActive when editing", () => {
expect(wrapper.find(`[data-test='auction-id-form-input']`).prop("disabled")).toBe(true);
expect(wrapper.find(`[data-test='active-option']`).prop("disabled")).toBe(true);
wrapper.setProps({
isEditing: false
});
expect(wrapper.find(`[data-test='auction-id-form-input']`).prop("disabled")).toBe(false);
expect(wrapper.find(`[data-test='active-option']`).prop("disabled")).toBe(false);
});
it("validates Auction ID", () => {
wrapper.setProps({
"isUniqueAuctionID" : false
})
wrapper.update();
expect(wrapper.find(`[data-test='auction-error']`).length).toBe(1);
});
it("validates Auction ID", () => {
wrapper.setProps({
"isUniqueAuctionID" : true,
"auctionHouseDetails": {
state: "WIS"
}
})
wrapper.update();
expect(wrapper.find(`[data-test='state-error']`).length).toBe(1);
});
});
@@ -0,0 +1,168 @@
import React from "react";
import FormInput, { FormInputType } from "../../../../components/form-input/FormInput";
import { IAuctionHouse } from "../../../../reducers/AuctionHouseReducer";
interface IAdminAuctionHouseFormProps {
auctionHouseDetails: IAuctionHouse;
handleInputChange: (inputKey: string, newValue: any) => void;
onSubmit: () => void;
isUniqueAuctionID: boolean;
isEditing: boolean;
disableSubmitButton: boolean;
}
const AdminAuctionHouseForm: React.FC<IAdminAuctionHouseFormProps> = (props) => {
return (
<div>
<div className="row mb-3">
<div className="container p-1 flex">
<div className="d-flex justify-content-between">
<div className="flex-grow-1">
<div className = "col pl-0">
<FormInput
label={"AuctionID from AutoIMS"}
type={FormInputType.Text}
value={props.auctionHouseDetails.auctionID}
onChange={(newValue)=>{props.handleInputChange("auctionID", newValue);}}
placeholder={"Name"}
required={!props.isEditing}
disabled={props.isEditing}
hasError={!props.isUniqueAuctionID}
data-test="auction-id-form-input"
/>
<p data-test="auction-error" className="mb-0 text-danger">{props.isUniqueAuctionID ? undefined : "AuctionID Must Be Unique"}</p>
</div>
<FormInput
label={"Auction House Name"}
type={FormInputType.Text}
value={props.auctionHouseDetails.name}
onChange={(newValue)=>{props.handleInputChange("name", newValue);}}
placeholder={"Name"}
required={true}
/>
</div>
<div className="flex-grow-1"/>
<div className="flex-grow-1">
<FormInput
label={"Phone Number"}
type={FormInputType.Text}
value={props.auctionHouseDetails.phone}
onChange={(newValue)=>{props.handleInputChange("phone", newValue);}}
placeholder={"Phone Number"}
required={true}
/>
<FormInput
label={"fax"}
type={FormInputType.Text}
value={props.auctionHouseDetails.fax}
onChange={(newValue)=>{props.handleInputChange("fax", newValue);}}
placeholder={"fax"}
required={true}
/>
</div>
</div>
<FormInput
label={"Address Line 1"}
type={FormInputType.Text}
value={props.auctionHouseDetails.address_Line_1}
onChange={(newValue)=>{props.handleInputChange("address_Line_1", newValue);}}
placeholder={"Address Line 1"}
required={true}
/>
<FormInput
label={"Address Line 2"}
type={FormInputType.Text}
value={props.auctionHouseDetails.address_Line_2}
onChange={(newValue)=>{props.handleInputChange("address_Line_2", newValue);}}
placeholder={"Address Line 2"}
/>
<div className="d-flex justify-content-between flex-grow-1">
<FormInput
label={"City"}
type={FormInputType.Text}
value={props.auctionHouseDetails.city}
onChange={(newValue)=>{props.handleInputChange("city", newValue);}}
placeholder={"City"}
required={true}
/>
<div className = "">
<FormInput
label={"State"}
type={FormInputType.Text}
value={props.auctionHouseDetails.state}
onChange={(newValue)=>{props.handleInputChange("state", newValue);}}
placeholder={"state"}
required={true}
hasError={(props.auctionHouseDetails.state.length > 2)}
data-test="auction-state-form-input"
/>
<p data-test="state-error" className="mb-0 text-danger">{(props.auctionHouseDetails.state.length > 2)? "Use Abbreviated State" : undefined}</p>
</div>
<FormInput
label={"Zipcode"}
type={FormInputType.Number}
value={props.auctionHouseDetails.zip}
onChange={(newValue)=>{props.handleInputChange("zip", newValue);}}
placeholder={"Zipcode"}
required={true}
/>
</div>
<div className="d-flex justify-content-left">
<FormInput
label={"Country"}
type={FormInputType.Text}
value={props.auctionHouseDetails.country}
onChange={(newValue)=>{props.handleInputChange("country", newValue);}}
placeholder={"Country"}
required={true}
/>
</div>
<FormInput
label={"Latitude"}
type={FormInputType.Text}
value={props.auctionHouseDetails.latitude}
onChange={(newValue)=>{props.handleInputChange("latitude", newValue);}}
placeholder={"Latitude"}
/>
<FormInput
label={"Longitude"}
type={FormInputType.Text}
value={props.auctionHouseDetails.longitude}
onChange={(newValue)=>{props.handleInputChange("longitude", newValue);}}
placeholder={"Longitude"}
/>
<div className="d-flex justify-content-between">
<FormInput
label={"Active"}
type={FormInputType.Selection}
value={props.auctionHouseDetails.active}
onChange={(newValue)=>{props.handleInputChange("active", newValue);}}
placeholder={"...pick one"}
options={
[
{value: "yes", display: "yes"},
{value: "no", display: "no"},
]
}
required={!props.isEditing}
disabled={props.isEditing}
data-test={"active-option"}
/>
<button
className="btn btn-primary mt-3"
onClick={() =>{props.onSubmit();}}
data-test="submit-auction-house"
disabled={props.disableSubmitButton}
>
Submit
</button>
</div>
</div>
</div>
</div>
);
};
export default AdminAuctionHouseForm;
@@ -0,0 +1,34 @@
import React from 'react';
import { shallow } from 'enzyme';
import AdminAuctionHousesRow from './AdminAuctionHouseRow';
import { IAuctionHouse } from '../../../reducers/AuctionHouseReducer';
describe('AdminAuctionHousesTable Component', () => {
let clickedAuctionHouse:any = undefined;
it ('Edit Button Returns Clicked Row', () => {
const wrapper = shallow(
<AdminAuctionHousesRow
auctionHouse={{
auctionID: "55555",
name: "Test",
active: "Test",
address_Line_1: "Test",
address_Line_2: "",
city: "Test",
state: "WI",
zip: 200202,
country: "Test",
phone: "10101010101",
fax: "10101010101",
latitude: undefined,
longitude: undefined
}}
selectForEdit={(auctionHouse: IAuctionHouse)=>{clickedAuctionHouse = auctionHouse}}
/>
);
expect(clickedAuctionHouse).toBe(undefined);
const editButton = wrapper.find("button");
editButton.simulate("Click");
expect(clickedAuctionHouse.auctionID).toBe("55555");
});
});
@@ -0,0 +1,35 @@
import React from "react";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
interface IAdminAuctionHouseRowProps {
auctionHouse: IAuctionHouse;
selectForEdit: (auctionHouse: IAuctionHouse) => any;
}
const AdminAuctionHouseRow: React.FC<IAdminAuctionHouseRowProps> = (props) => {
return (
<tr>
<td>{props.auctionHouse.name}</td>
<td>{props.auctionHouse.address_Line_1}</td>
<td>{props.auctionHouse.address_Line_2}</td>
<td>{props.auctionHouse.city}</td>
<td>{props.auctionHouse.state}</td>
<td>{props.auctionHouse.zip}</td>
<td>{props.auctionHouse.country}</td>
<td>{props.auctionHouse.phone}</td>
<td>{props.auctionHouse.fax}</td>
<td>{props.auctionHouse.active}</td>
<td>
<button
className="btn btn-primary btn-sm"
onClick={()=>{props.selectForEdit(props.auctionHouse);}}
>
Edit
</button>
</td>
</tr>
);
};
export default AdminAuctionHouseRow;
@@ -0,0 +1,34 @@
import React from 'react';
import { shallow } from 'enzyme';
import AdminAuctionHousesTable from './AdminAuctionHousesTable';
import { IAuctionHouse } from '../../../reducers/AuctionHouseReducer';
describe('AdminAuctionHousesTable Component', () => {
it ('Renders No Rows When Array is Empty', () => {
const wrapper = shallow(
<AdminAuctionHousesTable
auctionHouses={[]}
selectForEdit={(auctionHouse: IAuctionHouse)=>{}}
/>
);
const auctionHouseRows = wrapper.find(`[data-test='admin-auction-houses-table-row']`);
expect(auctionHouseRows.length).toBe(0);
});
it ('Renders Rows When Array is Non-Empty', () => {
const wrapper = shallow(
<AdminAuctionHousesTable
auctionHouses={[
{ auctionID: '1' } as IAuctionHouse,
{ auctionID: '2' } as IAuctionHouse,
{ auctionID: '3' } as IAuctionHouse,
]}
selectForEdit={(auctionHouse: IAuctionHouse)=>{}}
/>
);
const auctionHouseRows = wrapper.find(`[data-test='admin-auction-houses-table-row']`);
expect(auctionHouseRows.length).toBe(3);
});
});
@@ -0,0 +1,46 @@
import React from "react";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
import AdminAuctionHouseRow from "../admin-auction-house-row/AdminAuctionHouseRow";
interface IAdminAuctionHousesTableProps {
auctionHouses: IAuctionHouse[];
selectForEdit: (auctionHouse: IAuctionHouse) => any;
}
const AdminAuctionHousesTable: React.FC<IAdminAuctionHousesTableProps> = (props) => {
return (
<div>
<table className="table table-striped">
<thead className="thead-light">
<tr>
<th scope="col">Name</th>
<th scope="col">Address #1</th>
<th scope="col">Address #2</th>
<th scope="col">City</th>
<th scope="col">State</th>
<th scope="col">Zip</th>
<th scope="col">Country</th>
<th scope="col">Phone</th>
<th scope="col">Fax</th>
<th scope="col">Is Active</th>
<th scope="col">Edit</th>
</tr>
</thead>
<tbody>
{props.auctionHouses.map((auctionHouse) =>
(
<AdminAuctionHouseRow
key={auctionHouse.auctionID}
auctionHouse={auctionHouse}
selectForEdit={(auctionHouse: IAuctionHouse) => props.selectForEdit(auctionHouse)}
data-test="admin-auction-houses-table-row"
/>
)
)}
</tbody>
</table>
</div>
);
};
export default AdminAuctionHousesTable;
@@ -0,0 +1,3 @@
.dashboard-factoids {
margin-bottom: 1em;
}
@@ -0,0 +1,77 @@
import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps } from "react-router";
import {getAllFactoids, setSelectedFactoidIndex} from "../../actions/FactoidActions";
import { getSelectedFactoidAccounts } from "../../selectors/FactoidSelector";
import { IFactoid } from "../../services/service-types/FactoidTypes";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import Routes from "../../shared/Routes";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AccountListTable from "../account-list-view/account-list-table/AccountListTable";
import "./AdminDashboard.css";
import FactoidWidgetView from "./factoid-widget-view/FactoidWidgetView";
interface IPropsFromState {
factoids: IAsyncStateContainer<IFactoid[]>;
selectedFactoidIndex: number;
selectedFactoidAccounts: IRepoAccount[];
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
factoids: state.factoidState.factoids,
selectedFactoidIndex: state.factoidState.selectedFactoidIndex,
selectedFactoidAccounts: getSelectedFactoidAccounts(state)
};
};
interface IPropsFromDispatch {
getAllFactoids: () => any;
setFactoidIndex: (index: number) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
getAllFactoids: getAllFactoids,
setFactoidIndex: setSelectedFactoidIndex
};
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps;
const AdminDashBoard: React.FC<Props> = (props) => {
const goToAccount = (account: IRepoAccount) => {
props.history.push(Routes.Account(account.repoId));
};
const setSelectedFactoid = (index: number) => {
props.setFactoidIndex(index);
};
const loadFactoids = () => {
if (props.factoids.state === AsyncState.Inactive) {
props.getAllFactoids();
}
};
loadFactoids();
return(
<div className="main-panel">
<div className="container dashboard-factoids">
<FactoidWidgetView
factoids={props.factoids}
onFactoidSelected={setSelectedFactoid}
selectedWidgetIndex={props.selectedFactoidIndex}
/>
</div>
<AccountListTable
repoAccounts={props.selectedFactoidAccounts}
onRepoAccountClicked={goToAccount}
/>
</div>
);
};
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminDashBoard);
@@ -0,0 +1,5 @@
.factoid-wdiget-view {
.factoid-widget-parent {
padding: 0.25em;
}
}
@@ -0,0 +1,74 @@
import React from 'react';
import { shallow } from 'enzyme';
import FactoidWidgetView from './FactoidWidgetView';
import AsyncState from '../../../shared/enumerations/AsyncState';
import { IFactoid } from '../../../services/service-types/FactoidTypes';
describe('FactoidWidgetView Component', () => {
it ('Renders Notification Bar When Status is Not Success', () => {
const wrapper = shallow(
<FactoidWidgetView
factoids={{
state: AsyncState.Inactive,
data: [
{} as IFactoid,
{} as IFactoid
]
}}
onFactoidSelected={() => {}}
/>
);
const notificationBar = wrapper.find(`[data-test='factoid-widget-view-notification-bar']`);
expect(notificationBar.length).toBe(1);
});
it ('Renders Notification Bar When Data is Undefined', () => {
const wrapper = shallow(
<FactoidWidgetView
factoids={{
state: AsyncState.Success,
}}
onFactoidSelected={() => {}}
/>
);
const notificationBar = wrapper.find(`[data-test='factoid-widget-view-notification-bar']`);
expect(notificationBar.length).toBe(1);
});
it ('Renders Proper amount of Factoids', () => {
const wrapper = shallow(
<FactoidWidgetView
factoids={{
state: AsyncState.Success,
data: [
{} as IFactoid,
{} as IFactoid
]
}}
onFactoidSelected={() => {}}
/>
);
const factoidWidgets = wrapper.find(`[data-test='factoid-widget']`);
expect(factoidWidgets.length).toBe(2);
});
it ('Calls onFactoidSelected() on Click', () => {
let wasCallbackTriggered = false;
const wrapper = shallow(
<FactoidWidgetView
factoids={{
state: AsyncState.Success,
data: [
{} as IFactoid
]
}}
onFactoidSelected={() => {wasCallbackTriggered = true}}
/>
);
const factoidWidgets = wrapper.find(`[data-test='factoid-widget-parent']`);
factoidWidgets.simulate('click');
expect(wasCallbackTriggered).toBe(true);
});
});
@@ -0,0 +1,47 @@
import React from 'react';
import FactoidWidget from './factoid-widget/FactoidWidget';
import IAsyncStateContainer from '../../../shared/types/IAsyncStateContainer';
import './FactoidWidgetView.scss';
import { IFactoid } from '../../../services/service-types/FactoidTypes';
import AsyncState, { mapAsyncStateToNotificationType } from '../../../shared/enumerations/AsyncState';
import NotificationBar from '../../../components/notification-bar/NotificationBar';
interface IFactoidWidgetViewProps {
factoids: IAsyncStateContainer<IFactoid[]>;
selectedWidgetIndex?: number;
onFactoidSelected: (index: number) => any;
}
const FactoidWidgetView: React.FC<IFactoidWidgetViewProps> = (props) => {
if (props.factoids.state !== AsyncState.Success || !props.factoids.data) {
return (
<NotificationBar
type={mapAsyncStateToNotificationType(props.factoids.state)}
text={props.factoids.state}
data-test="factoid-widget-view-notification-bar"
/>
);
}
else {
return (
<div className="factoid-wdiget-view row">
{props.factoids.data.map((factoid, index) =>
<div
className="col factoid-widget-parent"
key={index}
onClick={() => props.onFactoidSelected(index)}
data-test="factoid-widget-parent"
>
<FactoidWidget
factoid={factoid}
isWidgetSelected={props.selectedWidgetIndex === index}
data-test="factoid-widget"
/>
</div>
)}
</div>
);
}
}
export default FactoidWidgetView;
@@ -0,0 +1,52 @@
@import 'node_modules/bootstrap/scss/functions';
@import 'node_modules/bootstrap/scss/variables';
@import 'node_modules/bootstrap/scss/mixins';
$factoiBorderRadius: 1em;
.factoid-widget {
border-radius: $factoiBorderRadius;
border-width: 2px;
cursor: pointer;
overflow: hidden;
width: 15em;
&.widget-selected {
border-color: rgba($success, 0.5);
}
&:hover {
border-color: rgba($primary, 0.5);
}
p, h3 {
margin: 0;
}
.factoid-main-body {
padding: 0.75em;
height: 6.5em;
}
.list-group-item {
background-color: transparent;
padding: 0.25em;
}
.list-group-item p {
text-align: center;
}
.factoid-icon {
font-size: 3em;
text-align: center;
}
.factoid-is-loading {
opacity: 0.25;
}
.icon-info-overlay {
position: absolute;
width: 100%;
.icon-info-overlay-icon {
font-size: 5em;
opacity: 0.5;
text-align: center;
}
}
}
@@ -0,0 +1,46 @@
import React from 'react';
import { ShallowWrapper, shallow } from 'enzyme';
import FactoidWidget from './FactoidWidget';
import { IFactoid } from '../../../../services/service-types/FactoidTypes';
describe('FactoidWidget Component', () => {
let wrapper: ShallowWrapper;
const dummyFactoid: IFactoid = {
cumulativeValue: '42',
name: 'Items',
fontAwesomeIcon: 'fa-car',
description: 'These are items',
factoidAccountIds: []
};
beforeEach(() => {
wrapper = shallow(
<FactoidWidget factoid={dummyFactoid} isWidgetSelected={false} />
);
});
it ('Renders Labels Correctly', () => {
const valueComponent = wrapper.find(`[data-test='factoid-value']`);
const nameComponent = wrapper.find(`[data-test='factoid-name']`);
const descriptionComponent = wrapper.find(`[data-test='factoid-description']`);
expect(valueComponent.text()).toEqual('42');
expect(nameComponent.text()).toEqual('Items');
expect(descriptionComponent.text()).toEqual('These are items');
});
it ('Renders Fontawesome Correctly', () => {
const fontawesomeComponent = wrapper.find(`[data-test='font-awesome-icon']`);
expect(fontawesomeComponent.hasClass('fa-car')).toBe(true);
});
it ('Has Selected CSS Class When Selected', () => {
wrapper = wrapper.setProps({
isWidgetSelected: true
});
const factoidCard = wrapper.find(`[data-test='factoid-card']`);
expect(factoidCard.hasClass('widget-selected')).toBe(true);
});
});
@@ -0,0 +1,38 @@
import React from 'react';
import './FactoidWidget.scss';
import { IFactoid } from '../../../../services/service-types/FactoidTypes';
export interface IFactoidWidgetProps {
factoid: IFactoid;
isWidgetSelected: boolean;
}
const FactoidWidget: React.FC<IFactoidWidgetProps> = (props) => {
const factoid = props.factoid;
return (
<div className={'factoid-widget card' + (props.isWidgetSelected ? ' widget-selected' : '')} data-test="factoid-card">
<div>
<div className="factoid-main-body container">
<div className="row h-100 justify-content-center align-items-center">
<div className="col-6">
<h1 className="factoid-icon" aria-hidden="true">
<i className={'fa ' + factoid.fontAwesomeIcon} data-test="font-awesome-icon"></i>
</h1>
</div>
<div className="col-6">
<h3 data-test="factoid-value">{factoid.cumulativeValue}</h3>
<p data-test="factoid-name">{factoid.name}</p>
</div>
</div>
</div>
<ul className="list-group list-group-flush">
<li className="list-group-item">
<p data-test="factoid-description">{factoid.description}</p>
</li>
</ul>
</div>
</div>
);
};
export default FactoidWidget;
@@ -0,0 +1,70 @@
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { getAllRepoCompanies, postRepoCompany, putRepoCompany } from "../../actions/RepoCompanyActions";
import NotificationBar from "../../components/notification-bar/NotificationBar";
import { IRepoCompany } from "../../reducers/RepoCompanyReducer";
import AsyncState, { mapAsyncStateToNotificationType } from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AdminRepoCompaniesTable from "./admin-repo-companies-table/AdminRepoCompaniesTable";
interface IPropsFromState {
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoCompanies: state.repoCompanyState.repoCompanies,
};
};
interface IPropsFromDispatch {
loadRepoCompanies: () => any;
putRepoCompany: (repoCompany: IRepoCompany) => any;
saveRepoCompany: (repoCompany: IRepoCompany) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
loadRepoCompanies: getAllRepoCompanies,
putRepoCompany: putRepoCompany,
saveRepoCompany: postRepoCompany
};
type Props = IPropsFromState & IPropsFromDispatch;
const AdminRepoCompanies: React.FC<Props> = (props) => {
useEffect(() => {
if (props.repoCompanies.state === AsyncState.Inactive) {
props.loadRepoCompanies();
}
});
const getNotificationBarType = () => {
return mapAsyncStateToNotificationType(props.repoCompanies.state);
};
return (
<div className="container-fluid main-panel" data-test="vehicle-floor-pricing-parameters">
{ !props.repoCompanies.data
?
<NotificationBar
type={getNotificationBarType()}
text={props.repoCompanies.state}
data-test="admin-repo-companies-notification-bar"
/>
:
<AdminRepoCompaniesTable
repoCompanies={props.repoCompanies.data}
putRepoCompany={props.putRepoCompany}
saveRepoCompany={props.saveRepoCompany}
data-test="admin-auction-houses-table"
/>
}
</div>
);
};
export const _AdminAuctionHouses = AdminRepoCompanies;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminRepoCompanies);
@@ -0,0 +1,7 @@
.admin-repo-companies-table {
overflow-x: auto;
}
.admin-repo-companies-table th {
min-width: 7.5em;
}
@@ -0,0 +1,51 @@
import React from "react";
import { IRepoCompany } from "../../../reducers/RepoCompanyReducer";
import AdminRepoCompanyCreation from "./admin-repo-company-creation/AdminRepoCompanyCreation";
import AdminRepoCompanyRow from "./admin-repo-company-row/AdminRepoCompanyRow";
import "./AdminRepoCompaniesTable.css";
interface IAdminRepoCompaniesTableProps {
repoCompanies: IRepoCompany[];
putRepoCompany: (repoCompany: IRepoCompany) => any;
saveRepoCompany: (repoCompany: IRepoCompany) => any;
}
const AdminRepoCompaniesTable: React.FC<IAdminRepoCompaniesTableProps> = (props) => {
return (
<table className="admin-repo-companies-table table table-striped">
<thead className="thead-light">
<tr>
<th scope="col">Name</th>
<th scope="col">ReferenceID</th>
<th scope="col">Email</th>
<th scope="col">Address #1</th>
<th scope="col">Address #2</th>
<th scope="col">City</th>
<th scope="col">State</th>
<th scope="col">ZIP</th>
<th scope="col">Country</th>
<th scope="col">Phone</th>
<th scope="col">Fax</th>
<th scope="col">Active</th>
<th className="button-actions-column" scope="col">Actions</th>
</tr>
</thead>
<tbody>
{props.repoCompanies.map((repoCompany) =>
<AdminRepoCompanyRow
key={repoCompany.id}
repoCompany={repoCompany}
putRepoCompany={props.putRepoCompany}
data-test="admin-repo-company-table-row"
/>
)}
</tbody>
<tbody>
<AdminRepoCompanyCreation saveRepoCompany={props.saveRepoCompany} />
</tbody>
</table>
);
};
export default AdminRepoCompaniesTable;
@@ -0,0 +1,3 @@
.admin-repo-company-creation-row {
background-color: transparent !important;
}
@@ -0,0 +1,61 @@
import { shallow } from "enzyme";
import React from "react";
import { IRepoCompany } from "../../../../reducers/RepoCompanyReducer";
import AdminRepoCompanyCreation from "./AdminRepoCompanyCreation";
describe("AdminRepoCompanyCreation Component", () => {
it("Updates & Calls Save Method", () => {
let savedRepoCompany: IRepoCompany | undefined;
// Arrange
const wrapper = shallow(
<AdminRepoCompanyCreation
saveRepoCompany={(x) => savedRepoCompany = x}
/>
);
const nameField = wrapper.find(`[data-test='name-field']`);
const referenceidField = wrapper.find(`[data-test='referenceid-field']`);
const emailField = wrapper.find(`[data-test='email-field']`);
const addr1Field = wrapper.find(`[data-test='addr-1-field']`);
const addr2Field = wrapper.find(`[data-test='addr-2-field']`);
const cityField = wrapper.find(`[data-test='city-field']`);
const stateField = wrapper.find(`[data-test='state-field']`);
const zipField = wrapper.find(`[data-test='zip-field']`);
const countryField = wrapper.find(`[data-test='country-field']`);
const phoneField = wrapper.find(`[data-test='phone-field']`);
const faxField = wrapper.find(`[data-test='fax-field']`);
// Act
nameField.simulate("change", "ABC Co.");
referenceidField.simulate("change", 2);
emailField.simulate("change", "[email protected]");
addr1Field.simulate("change", "1234 Main St.");
addr2Field.simulate("change", "#414");
cityField.simulate("change", "MKE");
stateField.simulate("change", "WI");
zipField.simulate("change", "53202");
countryField.simulate("change", "USA");
phoneField.simulate("change", "555-123-4567");
faxField.simulate("change", "555-111-2222");
const createButton = wrapper.find(`[data-test='create-btn']`);
createButton.simulate("click");
// Assert
expect(savedRepoCompany).not.toBe(undefined);
expect(savedRepoCompany!.referenceId).toBe(2);
expect(savedRepoCompany!.name).toBe("ABC Co.");
expect(savedRepoCompany!.email).toBe("[email protected]");
expect(savedRepoCompany!.address_Line_1).toBe("1234 Main St.");
expect(savedRepoCompany!.address_Line_2).toBe("#414");
expect(savedRepoCompany!.city).toBe("MKE");
expect(savedRepoCompany!.state).toBe("WI");
expect(savedRepoCompany!.zip).toBe("53202");
expect(savedRepoCompany!.country).toBe("USA");
expect(savedRepoCompany!.phone).toBe("555-123-4567");
expect(savedRepoCompany!.fax).toBe("555-111-2222");
});
});
@@ -0,0 +1,169 @@
import React from "react";
import FormInput, { FormInputType } from "../../../../components/form-input/FormInput";
import { IRepoCompany } from "../../../../reducers/RepoCompanyReducer";
import "./AdminRepoCompanyCreation.css";
interface IAdminRepoCompanyCreationProps {
saveRepoCompany: (repoCompany: IRepoCompany) => any;
}
interface IAdminRepoCompanyCreationState {
newRepoCompany: IRepoCompany;
}
class AdminRepoCompanyCreation extends React.Component<IAdminRepoCompanyCreationProps, IAdminRepoCompanyCreationState> {
constructor(props: IAdminRepoCompanyCreationProps) {
super(props);
this.state = {
newRepoCompany: {
name: ""
} as IRepoCompany
};
this.saveRepoCompany = this.saveRepoCompany.bind(this);
}
applyChange(updater: (repoCompany: IRepoCompany) => any) {
const copiedRepoCompany = {...this.state.newRepoCompany};
updater(copiedRepoCompany);
this.setState({
newRepoCompany: copiedRepoCompany
});
}
isFormValid(): boolean {
return this.state.newRepoCompany.name.trim() !== "";
}
saveRepoCompany(): void {
if (this.isFormValid()) {
this.props.saveRepoCompany(this.state.newRepoCompany!);
}
}
render() {
return (
<tr className="admin-repo-company-creation-row">
<td>
<FormInput
type={FormInputType.Text}
placeholder="New Repo Company LLC"
value={this.state.newRepoCompany.name}
onChange={(value) => this.applyChange(repoCompany => repoCompany.name = value)}
data-test="name-field"
/>
</td>
<td>
<FormInput
placeholder="ReferenceId"
type={FormInputType.Number}
value={this.state.newRepoCompany.referenceId}
onChange={(value) => this.applyChange(repoCompany => repoCompany.referenceId = value)}
data-test="referenceid-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="[email protected]"
value={this.state.newRepoCompany.email}
onChange={(value) => this.applyChange(repoCompany => repoCompany.email = value)}
data-test="email-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="Address #1"
value={this.state.newRepoCompany.address_Line_1}
onChange={(value) => this.applyChange(repoCompany => repoCompany.address_Line_1 = value)}
data-test="addr-1-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="Address #2"
value={this.state.newRepoCompany.address_Line_2}
onChange={(value) => this.applyChange(repoCompany => repoCompany.address_Line_2 = value)}
data-test="addr-2-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="City"
value={this.state.newRepoCompany.city}
onChange={(value) => this.applyChange(repoCompany => repoCompany.city = value)}
data-test="city-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="State"
value={this.state.newRepoCompany.state}
onChange={(value) => this.applyChange(repoCompany => repoCompany.state = value)}
data-test="state-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="ZIP"
value={this.state.newRepoCompany.zip}
onChange={(value) => this.applyChange(repoCompany => repoCompany.zip = value)}
data-test="zip-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="Country"
value={this.state.newRepoCompany.country}
onChange={(value) => this.applyChange(repoCompany => repoCompany.country = value)}
data-test="country-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="Phone"
value={this.state.newRepoCompany.phone}
onChange={(value) => this.applyChange(repoCompany => repoCompany.phone = value)}
data-test="phone-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Text}
placeholder="Fax"
value={this.state.newRepoCompany.fax}
onChange={(value) => this.applyChange(repoCompany => repoCompany.fax = value)}
data-test="fax-field"
/>
</td>
<td>
<p>Yes</p>
</td>
<td>
<button
type="button"
className="btn btn-success"
disabled={!this.isFormValid()}
onClick={this.saveRepoCompany}
data-test="create-btn"
>
<i className="fa fa-plus" aria-hidden="true"/>
&nbsp;
Create
</button>
</td>
</tr>
);
}
}
export default AdminRepoCompanyCreation;
@@ -0,0 +1,7 @@
.admin-repo-company-row-form button {
margin-right: 0.5em;
}
.disabled-repo-company-row p {
opacity: 0.5;
}
@@ -0,0 +1,110 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IRepoCompany } from "../../../../reducers/RepoCompanyReducer";
import AdminRepoCompanyRow from "./AdminRepoCompanyRow";
describe("AdminRepoCompanyCreation Component", () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<AdminRepoCompanyRow
repoCompany={{
id: 1,
name: "ABC Co.",
referenceId: 2,
email: "[email protected]",
address_Line_1: "1234 Main St.",
address_Line_2: "#414",
city: "MKE",
state: "WI",
zip: "53202",
country: "USA",
phone: "555-123-4567",
fax: "555-987-6543",
disabledDate: moment()
}}
putRepoCompany={(_) => {}}
/>
);
});
it("Renders Only Labels When Not Edited", () => {
const labelRow = wrapper.find(`[data-test='label-version']`);
expect(labelRow.length).toBe(1);
const inputRow = wrapper.find(`[data-test='input-version']`);
expect(inputRow.length).toBe(0);
});
it("Renders Only Inputs When Edited", () => {
const editRowBtn = wrapper.find(`[data-test='edit-row-btn']`);
editRowBtn.simulate("click");
const labelRow = wrapper.find(`[data-test='label-version']`);
expect(labelRow.length).toBe(0);
const inputRow = wrapper.find(`[data-test='input-version']`);
expect(inputRow.length).toBe(1);
});
it("Renders Fieids Correctly When Edited", () => {
// Arrange
const editRowBtn = wrapper.find(`[data-test='edit-row-btn']`);
editRowBtn.simulate("click");
const nameField = wrapper.find(`[data-test='name-field']`);
const referenceidField = wrapper.find(`[data-test='referenceid-field']`);
const emailField = wrapper.find(`[data-test='email-field']`);
const addr1Field = wrapper.find(`[data-test='addr-1-field']`);
const addr2Field = wrapper.find(`[data-test='addr-2-field']`);
const cityField = wrapper.find(`[data-test='city-field']`);
const stateField = wrapper.find(`[data-test='state-field']`);
const zipField = wrapper.find(`[data-test='zip-field']`);
const countryField = wrapper.find(`[data-test='country-field']`);
const phoneField = wrapper.find(`[data-test='phone-field']`);
const faxField = wrapper.find(`[data-test='fax-field']`);
const isActiveField = wrapper.find(`[data-test='is-active-field']`);
// Assert
expect(nameField.props().value).toBe("ABC Co.");
expect(referenceidField.props().value).toBe(2);
expect(emailField.props().value).toBe("[email protected]");
expect(addr1Field.props().value).toBe("1234 Main St.");
expect(addr2Field.props().value).toBe("#414");
expect(cityField.props().value).toBe("MKE");
expect(stateField.props().value).toBe("WI");
expect(zipField.props().value).toBe("53202");
expect(countryField.props().value).toBe("USA");
expect(phoneField.props().value).toBe("555-123-4567");
expect(faxField.props().value).toBe("555-987-6543");
expect(isActiveField.props().value).toBe(0);
});
it("Save Method Callback Triggered", () => {
// Arrange
let updatedRepoCompany: IRepoCompany | undefined;
wrapper.setProps({
putRepoCompany: (x: IRepoCompany) => updatedRepoCompany = x
});
const editRowBtn = wrapper.find(`[data-test='edit-row-btn']`);
editRowBtn.simulate("click");
const nameField = wrapper.find(`[data-test='name-field']`);
// Act
nameField.simulate("change", "New Company!");
const saveRowBtn = wrapper.find(`[data-test='save-row-btn']`);
saveRowBtn.simulate("click");
// Assert
expect(updatedRepoCompany).not.toBeUndefined();
expect(updatedRepoCompany!.name).toEqual("New Company!");
});
});
@@ -0,0 +1,280 @@
import moment from "moment";
import React from "react";
import FormInput, { FormInputType } from "../../../../components/form-input/FormInput";
import { IRepoCompany } from "../../../../reducers/RepoCompanyReducer";
import "./AdminRepoCompanyRow.css";
interface IRepoCompanyRowProps {
repoCompany: IRepoCompany;
putRepoCompany: (repoCompany: IRepoCompany) => any;
}
interface IRepoCompanyRowState {
editedRepoCompany?: IRepoCompany;
}
class AdminRepoCompanyRow extends React.Component<IRepoCompanyRowProps, IRepoCompanyRowState> {
constructor(props: IRepoCompanyRowProps) {
super(props);
this.state = {};
this.applyIsActiveChange = this.applyIsActiveChange.bind(this);
}
componentDidUpdate(prevProps: IRepoCompanyRowProps, prevState: IRepoCompanyRowState) {
if (prevState !== this.state) {
return;
}
if (prevProps.repoCompany !== this.props.repoCompany) {
this.setState({
editedRepoCompany: undefined
});
}
}
applyChange(updater: (repoCompany: IRepoCompany) => any) {
if (!this.state.editedRepoCompany) {
return;
}
const copiedRepoCompany = {...this.state.editedRepoCompany};
updater(copiedRepoCompany);
this.setState({
editedRepoCompany: copiedRepoCompany
});
}
applyIsActiveChange(value: string) {
const isActive = !!parseInt(value);
const disabledDate = isActive ? undefined : moment();
this.applyChange(repoCompany => repoCompany.disabledDate = disabledDate);
}
beginEditingRow() {
this.setState({
editedRepoCompany: {
...this.props.repoCompany
}
});
}
cancelEditingRow() {
this.setState({
editedRepoCompany: undefined
});
}
hasStringFieldChanged(fieldExtraction: (repoCompany: IRepoCompany) => string | null | undefined) {
const orig = fieldExtraction(this.props.repoCompany);
const edited = this.state.editedRepoCompany ? fieldExtraction(this.state.editedRepoCompany) : undefined;
return (orig || "") !== (edited || "");
}
isRepoCompanyAltered() {
if (!this.state.editedRepoCompany) {
return false;
}
return (
this.props.repoCompany.name !== this.state.editedRepoCompany.name ||
this.hasStringFieldChanged(repoCompany => repoCompany.email) ||
this.props.repoCompany.referenceId !== this.state.editedRepoCompany.referenceId ||
this.hasStringFieldChanged(repoCompany => repoCompany.address_Line_1) ||
this.hasStringFieldChanged(repoCompany => repoCompany.address_Line_2) ||
this.hasStringFieldChanged(repoCompany => repoCompany.city) ||
this.hasStringFieldChanged(repoCompany => repoCompany.state) ||
this.hasStringFieldChanged(repoCompany => repoCompany.zip) ||
this.hasStringFieldChanged(repoCompany => repoCompany.country) ||
this.hasStringFieldChanged(repoCompany => repoCompany.phone) ||
this.hasStringFieldChanged(repoCompany => repoCompany.fax) ||
!!this.props.repoCompany.disabledDate !== !!this.state.editedRepoCompany.disabledDate
);
}
isRowDisabled(): boolean {
return !!this.props.repoCompany.disabledDate;
}
render() {
if (this.state.editedRepoCompany) {
const areStringInputsDisabled = !!this.state.editedRepoCompany.disabledDate;
return (
<tr className="admin-repo-company-row-form" data-test="input-version">
<td>
<FormInput
placeholder="Name"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.name}
onChange={(value) => this.applyChange(repoCompany => repoCompany.name = value)}
data-test="name-field"
/>
</td>
<td>
<FormInput
placeholder="ReferenceId"
type={FormInputType.Number}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.referenceId}
onChange={(value) => this.applyChange(repoCompany => repoCompany.referenceId = value)}
data-test="referenceid-field"
/>
</td>
<td>
<FormInput
placeholder="Email"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.email}
onChange={(value) => this.applyChange(repoCompany => repoCompany.email = value)}
data-test="email-field"
/>
</td>
<td>
<FormInput
placeholder="Address #1"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.address_Line_1}
onChange={(value) => this.applyChange(repoCompany => repoCompany.address_Line_1 = value)}
data-test="addr-1-field"
/>
</td>
<td>
<FormInput
placeholder="Address #2"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.address_Line_2}
onChange={(value) => this.applyChange(repoCompany => repoCompany.address_Line_2 = value)}
data-test="addr-2-field"
/>
</td>
<td>
<FormInput
placeholder="City"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.city}
onChange={(value) => this.applyChange(repoCompany => repoCompany.city = value)}
data-test="city-field"
/>
</td>
<td>
<FormInput
placeholder="State"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.state}
onChange={(value) => this.applyChange(repoCompany => repoCompany.state = value)}
data-test="state-field"
/>
</td>
<td>
<FormInput
placeholder="ZIP"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.zip}
onChange={(value) => this.applyChange(repoCompany => repoCompany.zip = value)}
data-test="zip-field"
/>
</td>
<td>
<FormInput
placeholder="Country"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.country}
onChange={(value) => this.applyChange(repoCompany => repoCompany.country = value)}
data-test="country-field"
/>
</td>
<td>
<FormInput
placeholder="Phone"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.phone}
onChange={(value) => this.applyChange(repoCompany => repoCompany.phone = value)}
data-test="phone-field"
/>
</td>
<td>
<FormInput
placeholder="Fax"
type={FormInputType.Text}
disabled={areStringInputsDisabled}
value={this.state.editedRepoCompany.fax}
onChange={(value) => this.applyChange(repoCompany => repoCompany.fax = value)}
data-test="fax-field"
/>
</td>
<td>
<FormInput
type={FormInputType.Selection}
value={!this.state.editedRepoCompany.disabledDate ? 1 : 0}
onChange={this.applyIsActiveChange}
data-test="is-active-field"
options={[
{ value: 1, display: "Yes" },
{ value: 0, display: "No" }
]}
/>
</td>
<td className="button-actions-column">
<button
type="button"
className="btn btn-success"
disabled={!this.isRepoCompanyAltered()}
onClick={() => this.props.putRepoCompany(this.state.editedRepoCompany!)}
data-test="save-row-btn"
>
<i className="fa fa-floppy-o" aria-hidden="true"/>
</button>
<button
type="button"
className="btn btn-warning"
onClick={() => this.cancelEditingRow()}
data-test="cancel-row-btn"
>
<i className="fa fa-ban" aria-hidden="true"/>
</button>
</td>
</tr>
);
} else {
return (
<tr className={this.isRowDisabled() ? "disabled-repo-company-row" : ""} data-test="label-version">
<td><p>{this.props.repoCompany.name}</p></td>
<td>{this.props.repoCompany.referenceId || "-"}</td>
<td><p>{this.props.repoCompany.email}</p></td>
<td><p>{this.props.repoCompany.address_Line_1 || "-"}</p></td>
<td><p>{this.props.repoCompany.address_Line_2 || "-"}</p></td>
<td><p>{this.props.repoCompany.city || "-"}</p></td>
<td><p>{this.props.repoCompany.state || "-"}</p></td>
<td><p>{this.props.repoCompany.zip || "-"}</p></td>
<td><p>{this.props.repoCompany.country || "-"}</p></td>
<td><p>{this.props.repoCompany.phone || "-"}</p></td>
<td><p>{this.props.repoCompany.fax || "-"}</p></td>
<td><p>{!this.props.repoCompany.disabledDate ? "Yes" : "No"}</p></td>
<td className="button-actions-column">
<button
type="button"
className="btn btn-primary"
onClick={() => this.beginEditingRow()}
data-test="edit-row-btn"
>
<i className="fa fa-pencil" aria-hidden="true"/>
</button>
</td>
</tr>
);
}
}
}
export default AdminRepoCompanyRow;
@@ -0,0 +1,3 @@
.admin-report-form {
padding: 1em;
}
@@ -0,0 +1,23 @@
import { shallow } from "enzyme";
import React from "react";
import AdminReportSelectionForm from "./AdminReportSelectionForm";
describe("AdminReportSelectionForm Container", () => {
it("should render the input form", () => {
const wrapper = shallow(
<AdminReportSelectionForm
reportForm={{}}
isExcelButtonDisabled={false}
availableReports={[]}
onReportFormChange={() => {}}
onExcelReportClick={() => {}}
/>
);
const mainForm = wrapper.find(`[data-test='admin_report_form']`);
const inputs = wrapper.find(`[data-test='date-input']`);
expect(mainForm.length).toBe(1);
expect(inputs.length).toBe(0);
});
});
@@ -0,0 +1,67 @@
import React, { useState } from "react";
import FormInput, { FormInputType } from "../../components/form-input/FormInput";
import { IAdminReportForm, IReportUrlDisplayMap } from "../../reducers/ReportingReducer";
import "./AdminReportSelectionForm.css";
interface IAdminReportSelectionFormProps {
readonly reportForm: IAdminReportForm;
readonly isExcelButtonDisabled: boolean;
readonly availableReports: IReportUrlDisplayMap[];
onReportFormChange: (reportForm: IAdminReportForm) => any;
onExcelReportClick: () => any;
}
type IAdminReportFormUpdater = (x: IAdminReportForm) => void;
const AdminReportSelectionForm: React.FC<IAdminReportSelectionFormProps> = (props) => {
const [url, setUrl] = useState("");
const applyChange = (updater: IAdminReportFormUpdater) => {
const copiedForm = Object.assign({}, props.reportForm);
updater(copiedForm);
props.onReportFormChange(copiedForm);
};
const handleReportSelectionChange = (url: string) => {
const foundUrlMap = props.availableReports.find(x => x.url === url);
if (foundUrlMap) {
applyChange((reportForm => reportForm.reportUrl = foundUrlMap));
}
setUrl(url)
};
return (
<div className="row admin-report-form" data-test="admin_report_form">
<div className="reportSelection">
<FormInput
type={FormInputType.Selection}
value={props.reportForm.reportUrl ? props.reportForm.reportUrl.url : ""}
onChange={handleReportSelectionChange}
placeholder="Select Report"
options={props.availableReports.map(availableReport => {
return{
value: availableReport.url,
display: availableReport.display
};
})}
data-test="reporting-auction-type"
/>
</div>
<div className="col">
<button
onClick={() => window.open(url)}
className="btn btn-primary"
data-test="export-report-btn"
>
Go To Report
</button>
</div>
</div>
);
};
export default AdminReportSelectionForm;
@@ -0,0 +1,61 @@
import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router";
import { selectReport } from "../../actions/ReportingActions";
import { IAdminReportForm, IReport } from "../../reducers/ReportingReducer";
import ExportService from "../../services/ExportService";
import ReportingService from "../../services/ReportingService";
import Routes from "../../shared/Routes";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AdminReportSelectionForm from "./AdminReportSelectionForm";
interface IPropsFromState {
readonly reportForm: IAdminReportForm;
readonly currentReport: IAsyncStateContainer<IReport>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
reportForm: state.reportingState.reportForm,
currentReport: state.reportingState.currentReport
};
};
interface IPropsFromDispatch {
readonly selectReport: (updatedSelection: IAdminReportForm) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
selectReport: selectReport
};
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps;
const AdminReporting: React.FC<Props> = (props) => {
const requestReportExport = () => {
if (props.currentReport.data && props.reportForm.reportUrl ) {
ExportService.ExportReport(props.currentReport.data, props.reportForm.reportUrl.display);
}
};
const goToAccount = (repoId: number) => {
props.history.push(Routes.Account(repoId));
};
return (
<div className="main-panel container-fluid">
<AdminReportSelectionForm
reportForm={props.reportForm}
isExcelButtonDisabled={!props.currentReport.data}
availableReports={ReportingService.GetAllReportUrls()}
onReportFormChange={props.selectReport}
onExcelReportClick={requestReportExport}
/>
</div>
);
};
export const _AdminReporting = AdminReporting;
export default withRouter(connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminReporting));
@@ -0,0 +1,239 @@
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import DataTable from "./DataTable";
describe("ReportTable Component", () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<DataTable
dataTable={{
headers: ["Header 1", "Header 2", "Header 3"],
rows: [
{
repoId: 1,
cells: [{ display: "Cell 1", value: 1 }, { display: "Cell 2", value: "B" }, { display: "Test Sort A"}]
},
{
repoId: 2,
cells: [{ display: "Cell 3" }, { display: "Cell 4", value: "A" }, { display: "Test Sort C"}]
},
{
repoId: 8,
cells: [{ display: "Cell 5", value: -1 }, { display: "Cell 6" }, { display: "Test Sort B"}]
}
]
}}
onDataTableClicked={() => {}}
/>
);
});
it ("Renders Headers", () => {
const headers = wrapper.find(`[data-test='header']`);
expect(headers.length).toBe(3);
expect(headers.at(0).text()).toEqual("Header 1");
expect(headers.at(1).text()).toEqual("Header 2");
expect(headers.at(2).text()).toEqual("Header 3");
});
it ("Renders Cells", () => {
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 1");
expect(cells.at(1).text()).toEqual("Cell 2");
expect(cells.at(2).text()).toEqual("Test Sort A");
// Row 2
expect(cells.at(3).text()).toEqual("Cell 3");
expect(cells.at(4).text()).toEqual("Cell 4");
expect(cells.at(5).text()).toEqual("Test Sort C");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 5");
expect(cells.at(7).text()).toEqual("Cell 6");
expect(cells.at(8).text()).toEqual("Test Sort B");
});
it ("Calls Row Callback", () => {
let repoIdSelected = -1;
wrapper.setProps({
onDataTableClicked: (repoId: number) => repoIdSelected = repoId
});
const rows = wrapper.find(`[data-test='data-row']`);
expect(rows.length).toBe(3);
rows.at(2).simulate("click");
expect(repoIdSelected).toBe(8);
});
it ("Sorts Column 1", () => {
wrapper = wrapper.setState({
sortedColumn: {
index: 0,
isReversed: false,
}
});
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 3");
expect(cells.at(1).text()).toEqual("Cell 4");
expect(cells.at(2).text()).toEqual("Test Sort C");
// Row 2
expect(cells.at(3).text()).toEqual("Cell 5");
expect(cells.at(4).text()).toEqual("Cell 6");
expect(cells.at(5).text()).toEqual("Test Sort B");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 1");
expect(cells.at(7).text()).toEqual("Cell 2");
expect(cells.at(8).text()).toEqual("Test Sort A");
});
it ("Sorts Column 1 (Reversed)", () => {
wrapper = wrapper.setState({
sortedColumn: {
index: 0,
isReversed: true,
}
});
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 1");
expect(cells.at(1).text()).toEqual("Cell 2");
expect(cells.at(2).text()).toEqual("Test Sort A");
// Row 2
expect(cells.at(3).text()).toEqual("Cell 5");
expect(cells.at(4).text()).toEqual("Cell 6");
expect(cells.at(5).text()).toEqual("Test Sort B");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 3");
expect(cells.at(7).text()).toEqual("Cell 4");
expect(cells.at(8).text()).toEqual("Test Sort C");
});
it ("Sorts Column 2", () => {
wrapper = wrapper.setState({
sortedColumn: {
index: 1,
isReversed: false,
}
});
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 5");
expect(cells.at(1).text()).toEqual("Cell 6");
expect(cells.at(2).text()).toEqual("Test Sort B");
// Row 2
expect(cells.at(3).text()).toEqual("Cell 3");
expect(cells.at(4).text()).toEqual("Cell 4");
expect(cells.at(5).text()).toEqual("Test Sort C");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 1");
expect(cells.at(7).text()).toEqual("Cell 2");
expect(cells.at(8).text()).toEqual("Test Sort A");
});
it ("Sorts Column 2 (Reversed)", () => {
wrapper = wrapper.setState({
sortedColumn: {
index: 1,
isReversed: true,
}
});
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 2
expect(cells.at(0).text()).toEqual("Cell 1");
expect(cells.at(1).text()).toEqual("Cell 2");
expect(cells.at(2).text()).toEqual("Test Sort A");
// Row 1
expect(cells.at(3).text()).toEqual("Cell 3");
expect(cells.at(4).text()).toEqual("Cell 4");
expect(cells.at(5).text()).toEqual("Test Sort C");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 5");
expect(cells.at(7).text()).toEqual("Cell 6");
expect(cells.at(8).text()).toEqual("Test Sort B");
});
it ("Sorts Column 3", () => {
wrapper = wrapper.setState({
sortedColumn: {
index: 2,
isReversed: false,
}
});
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 1");
expect(cells.at(1).text()).toEqual("Cell 2");
expect(cells.at(2).text()).toEqual("Test Sort A");
// Row 2
expect(cells.at(3).text()).toEqual("Cell 5");
expect(cells.at(4).text()).toEqual("Cell 6");
expect(cells.at(5).text()).toEqual("Test Sort B");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 3");
expect(cells.at(7).text()).toEqual("Cell 4");
expect(cells.at(8).text()).toEqual("Test Sort C");
});
it ("Sorts Column 3 (Reversed)", () => {
wrapper = wrapper.setState({
sortedColumn: {
index: 2,
isReversed: true,
}
});
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(9);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 3");
expect(cells.at(1).text()).toEqual("Cell 4");
expect(cells.at(2).text()).toEqual("Test Sort C");
// Row 2
expect(cells.at(3).text()).toEqual("Cell 5");
expect(cells.at(4).text()).toEqual("Cell 6");
expect(cells.at(5).text()).toEqual("Test Sort B");
// Row 3
expect(cells.at(6).text()).toEqual("Cell 1");
expect(cells.at(7).text()).toEqual("Cell 2");
expect(cells.at(8).text()).toEqual("Test Sort A");
});
});
@@ -0,0 +1,103 @@
import React from "react";
import { getCompareValueForCells, IDataTable } from "../../../../reducers/ReportingReducer";
import { IColumnSort } from "../../../../shared/types/IColumnSort";
const cloneDeep = require("lodash.clonedeep");
interface IDataTableProps {
dataTable: IDataTable;
onDataTableClicked: (repoId: number) => any;
}
interface IReportTableState {
sortedColumn?: IColumnSort;
}
class DataTable extends React.Component<IDataTableProps, IReportTableState> {
constructor(props: IDataTableProps) {
super(props);
this.state = {};
}
componentDidUpdate(prevProps: IDataTableProps, prevState: IReportTableState) {
if (prevState !== this.state) {
return;
}
if (prevProps.dataTable !== this.props.dataTable) {
this.setState({
sortedColumn: undefined
});
}
}
createdSortedTable(): IDataTable {
const newTable = cloneDeep(this.props.dataTable) as IDataTable;
if (!this.state.sortedColumn) {
return newTable;
}
const reverse = this.state.sortedColumn.isReversed ? -1 : 1;
newTable.rows.sort((a, b) => {
const index = this.state.sortedColumn!.index;
return getCompareValueForCells(a.cells[index], b.cells[index]) * reverse;
});
return newTable;
}
sortColumn(index: number) {
if (this.state.sortedColumn && this.state.sortedColumn.index === index) {
if (this.state.sortedColumn.isReversed) {
this.setState({
sortedColumn: undefined
});
} else {
this.setState({
sortedColumn: { index: index, isReversed: true }
});
}
} else {
this.setState({
sortedColumn: { index: index, isReversed: false }
});
}
}
render() {
const sortedTable = this.createdSortedTable();
return (
<table className="table table-striped table-hover" data-test="gross-amount-table-main">
<thead className="thead-light">
<tr>
{sortedTable.headers.map((header, index) =>
<th key={index} onClick={() => this.sortColumn(index)} className="report-display-sortable-header">
{ this.state.sortedColumn && this.state.sortedColumn.index === index && !this.state.sortedColumn.isReversed ?
<i className="fa fa-long-arrow-down" aria-hidden="true">&nbsp;</i>
: null}
{ this.state.sortedColumn && this.state.sortedColumn.index === index && this.state.sortedColumn.isReversed ?
<i className="fa fa-long-arrow-up" aria-hidden="true">&nbsp;</i>
: null}
<span data-test="header">{header}</span>
</th>
)}
</tr>
</thead>
<tbody data-test="gross-amount-table-body">
{sortedTable.rows.map((row, index) =>
<tr key={index} onClick={() => this.props.onDataTableClicked(row.repoId)} data-test="data-row">
{row.cells.map((cell, index) =>
<td key={index} data-test="cell">{cell.display}</td>
)}
</tr>
)}
</tbody>
</table>
);
}
}
export default DataTable;
@@ -0,0 +1,50 @@
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import ReportTable from "./ReportTable";
describe("ReportTable Component", () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<ReportTable
reportTable={{
headers: ["Header A", "Header B", "Header C"],
cells: [
[{ display: "Cell 1" }, { display: "Cell 2" }],
[{ display: "Cell 3" }, { display: "Cell 4" }],
[{ display: "Cell 5" }, { display: "Cell 6" }]
]
}}
/>
);
});
it ("Renders Headers", () => {
const headers = wrapper.find(`[data-test='header']`);
expect(headers.length).toBe(3);
expect(headers.at(0).text()).toEqual("Header A");
expect(headers.at(1).text()).toEqual("Header B");
expect(headers.at(2).text()).toEqual("Header C");
});
it ("Renders Cells", () => {
const cells = wrapper.find(`[data-test='cell']`);
expect(cells.length).toBe(6);
// Row 1
expect(cells.at(0).text()).toEqual("Cell 1");
expect(cells.at(1).text()).toEqual("Cell 2");
// Row 2
expect(cells.at(2).text()).toEqual("Cell 3");
expect(cells.at(3).text()).toEqual("Cell 4");
// Row 3
expect(cells.at(4).text()).toEqual("Cell 5");
expect(cells.at(5).text()).toEqual("Cell 6");
});
});
@@ -0,0 +1,156 @@
import React from "react";
import { getCompareValueForCells, IReportDataTableCell, IReportTable } from "../../../../reducers/ReportingReducer";
import { IColumnSort } from "../../../../shared/types/IColumnSort";
const cloneDeep = require("lodash.clonedeep");
interface IReportTableProps {
reportTable: IReportTable;
}
interface IReportTableState {
sortedColumn?: IColumnSort;
}
class ReportTable extends React.Component<IReportTableProps, IReportTableState> {
private minHSl = 120;
private maxHsl = 225;
constructor(props: IReportTableProps) {
super(props);
this.state = {};
}
componentDidUpdate(prevProps: IReportTableProps, prevState: IReportTableState) {
if (prevState !== this.state) {
return;
}
if (prevProps.reportTable !== this.props.reportTable) {
this.setState({
sortedColumn: undefined
});
}
}
createdSortedTable(): IReportTable {
const newTable = cloneDeep(this.props.reportTable) as IReportTable;
if (!this.state.sortedColumn) {
return newTable;
}
const reverse = this.state.sortedColumn.isReversed ? -1 : 1;
newTable.cells.sort((a, b) => {
const index = this.state.sortedColumn!.index;
return getCompareValueForCells(a[index], b[index]) * reverse;
});
return newTable;
}
sortColumn(index: number) {
if (this.state.sortedColumn && this.state.sortedColumn.index === index) {
if (this.state.sortedColumn.isReversed) {
this.setState({
sortedColumn: undefined
});
} else {
this.setState({
sortedColumn: { index: index, isReversed: true }
});
}
} else {
this.setState({
sortedColumn: { index: index, isReversed: false }
});
}
}
getCellBackgroundColor(cell: IReportDataTableCell, minValue?: number, maxValue?: number): React.CSSProperties | undefined {
if (minValue === undefined || maxValue === undefined) {
return;
}
if (cell.value && typeof cell.value === "number") {
const range = maxValue - minValue;
const relativeValue = cell.value + minValue;
if (range === 0) {
return;
}
const percentage = Math.abs(relativeValue / range);
const hue = (percentage * (this.maxHsl - this.minHSl)) + this.minHSl;
return {
backgroundColor: `hsl(${hue}, 100%, 90%)`
};
}
}
getMinCellValue(): number | undefined {
var min: number | undefined ;
for (const row of this.props.reportTable.cells) {
for (const cell of row) {
if (typeof cell.value === "number") {
if (min === undefined) {
min = cell.value;
} else if (cell.value < min) {
min = cell.value;
}
}
}
}
return min;
}
getMaxCellValue(): number | undefined {
var max: number | undefined ;
for (const row of this.props.reportTable.cells) {
for (const cell of row) {
if (typeof cell.value === "number") {
if (max === undefined) {
max = cell.value;
} else if (cell.value > max) {
max = cell.value;
}
}
}
}
return max;
}
render() {
const sortedTable = this.createdSortedTable();
const minValue = this.getMinCellValue();
const maxValue = this.getMaxCellValue();
return (
<table className="table" data-test="gross-amount-table-main">
<thead className="thead-light">
<tr>
{sortedTable.headers.map((header, index) =>
<th key={index} onClick={() => this.sortColumn(index)} className="report-display-sortable-header">
{ this.state.sortedColumn && this.state.sortedColumn.index === index && !this.state.sortedColumn.isReversed ?
<i className="fa fa-long-arrow-down" aria-hidden="true">&nbsp;</i>
: null}
{ this.state.sortedColumn && this.state.sortedColumn.index === index && this.state.sortedColumn.isReversed ?
<i className="fa fa-long-arrow-up" aria-hidden="true">&nbsp;</i>
: null}
<span data-test="header">{header}</span>
</th>
)}
</tr>
</thead>
<tbody data-test="gross-amount-table-body">
{sortedTable.cells.map((row, index) =>
<tr key={index}>
{row.map((cell, index) =>
<td key={index} style={this.getCellBackgroundColor(cell, minValue, maxValue)} data-test="cell">{cell.display}</td>
)}
</tr>
)}
</tbody>
</table>
);
}
}
export default ReportTable;
@@ -0,0 +1,4 @@
.main-panel {
padding: 75px 15px;
min-height: calc(101vh - 80px);
}
@@ -0,0 +1,31 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { shallow } from 'enzyme';
import {AlertBar} from "./AlertBar";
import AsyncState from '../../shared/enumerations/AsyncState';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('AlertBar Component', () => {
it ('renders four alert boxes', () => {
const wrapper = shallow(
<AlertBar
alerts={[]}
actions={[]}
accountFilters={{}}
repoCompanies={{ state: AsyncState.Inactive }}
auctionHouses={{ state: AsyncState.Inactive }}
possibleStatuses={[]}
fetchAuctionHouses={() => {}}
fetchRepoCompanies={() => {}}
fetchFilteredAccounts={() => {}}
/>
);
const component = wrapper.find(`[data-test='alertBox']`);
expect(component.length).toBe(2);
});
});
@@ -0,0 +1,211 @@
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { getAllActions, getAllAlerts } from "../../actions/AlertActions";
import { getAllAuctionHouses } from "../../actions/AuctionHouseActions";
import { fetchRepoAccountsViaFilters } from "../../actions/RepoAccountActions";
import { getAllRepoCompanies } from "../../actions/RepoCompanyActions";
import AlertBox from "../../components/alert-box/AlertBox";
import { IAlert } from "../../reducers/AlertReducer";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import { IRepoCompany } from "../../reducers/RepoCompanyReducer";
import { getAllPossibleStatuses } from "../../selectors/SettingsSelector";
import { IRepoAccountFilters } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AlertBarFilterDropdowns from "./alert-bar-filter-dropdowns/AlertBarFilterDropdowns";
import AlertBarFilterTags, { IAlertTag } from "./alert-bar-filter-tags/AlertBarFilterTags";
import "./AlertBar.css";
interface IPropsFromState {
alerts: IAlert[];
actions: IAlert[];
accountFilters: IRepoAccountFilters;
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
possibleStatuses: string[];
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
alerts: state.alertState.alerts,
actions: state.alertState.actions,
accountFilters: state.repoAccountState.accountFilters,
repoCompanies: state.repoCompanyState.repoCompanies,
auctionHouses: state.auctionHouseState.auctionHouses,
possibleStatuses: getAllPossibleStatuses(state)
};
};
interface IDispatchFromState {
fetchFilteredAccounts: (accountFilters: IRepoAccountFilters) => any;
fetchAuctionHouses: () => any;
fetchRepoCompanies: () => any;
getAllActions: () => any;
getAllAlerts: () => any;
}
const mapDispatchToProps: IDispatchFromState = {
fetchFilteredAccounts: fetchRepoAccountsViaFilters,
fetchAuctionHouses: getAllAuctionHouses,
fetchRepoCompanies: getAllRepoCompanies,
getAllActions: getAllActions,
getAllAlerts: getAllAlerts
};
type Props = IPropsFromState & IDispatchFromState;
export const AlertBar: React.FC<Props> = (props) => {
const {getAllActions, getAllAlerts} = props;
useEffect(() => {
getAllActions();
getAllAlerts();
},[getAllActions, getAllAlerts]);
const fetchDependencies = () => {
if (props.auctionHouses.state === AsyncState.Inactive) {
props.fetchAuctionHouses();
}
if (props.repoCompanies.state === AsyncState.Inactive) {
props.fetchRepoCompanies();
}
};
const eventAlertRowClicked = (message: string, repoIds: number[]) => {
postNewFilter({
...props.accountFilters,
alertMessage: message,
repoIdList: repoIds
});
};
const postNewFilter = (filter: IRepoAccountFilters) => {
props.fetchFilteredAccounts(filter);
};
const createFilterTags = (): IAlertTag[] => {
const filterTags: IAlertTag[] = [];
if (props.accountFilters.alertMessage) {
filterTags.push({
message: props.accountFilters.alertMessage,
onTagCleared: () => {
postNewFilter({
...props.accountFilters,
alertMessage: undefined,
repoIdList: []
});
}
});
}
if (props.accountFilters.status) {
filterTags.push({
message: props.accountFilters.status,
onTagCleared: () => {
postNewFilter({
...props.accountFilters,
status: undefined
});
}
});
}
if (props.accountFilters.repoCompanyId) {
filterTags.push({
message: createRepoCompanyTag(props.accountFilters.repoCompanyId),
onTagCleared: () => {
postNewFilter({
...props.accountFilters,
repoCompanyId: undefined
});
}
});
}
if (props.accountFilters.auctionHouseId) {
filterTags.push({
message: createAuctionTag(props.accountFilters.auctionHouseId),
onTagCleared: () => {
postNewFilter({
...props.accountFilters,
auctionHouseId: undefined
});
}
});
}
if (props.accountFilters.onlyOpenAccounts) {
filterTags.push({
message: props.accountFilters.onlyOpenAccounts? "Hide Closed Accounts" : "",
onTagCleared: () => {
postNewFilter({
...props.accountFilters,
onlyOpenAccounts: false
});
}
});
}
return filterTags;
};
const createRepoCompanyTag = (repoCompanyId: number): string => {
if (props.repoCompanies.data) {
const matchingRepoCompany = props.repoCompanies.data.find((repoCompany => repoCompany.id === repoCompanyId));
if (matchingRepoCompany) {
return matchingRepoCompany.name;
}
}
return "Missing";
};
const createAuctionTag = (auctionId: string): string => {
if (props.auctionHouses.data) {
const matchingAuctionName = props.auctionHouses.data.find((auctionHouse => auctionHouse.auctionID === auctionId));
if (matchingAuctionName) {
return matchingAuctionName.name;
}
}
return "Missing";
};
fetchDependencies();
return (
<div>
<div className="row">
<div className="col-md mb-3">
<AlertBox
alerts={props.alerts}
currentlySelectedAlertMessage={props.accountFilters.alertMessage}
title={"Unread Alert" + (props.alerts.length === 1 ? "" : "s")}
alertRowClicked={eventAlertRowClicked}
data-test="alertBox"
/>
</div>
<div className="col-md mb-3">
<AlertBox
alerts={props.actions}
currentlySelectedAlertMessage={props.accountFilters.alertMessage}
title={"Action" + (props.alerts.length === 1 ? "" : "s") + " Needed"}
alertRowClicked={eventAlertRowClicked}
data-test="alertBox"
/>
</div>
<div className="col-md mb-3">
<AlertBarFilterTags filterTags={createFilterTags()} />
</div>
</div>
<AlertBarFilterDropdowns
accountFilters={props.accountFilters}
statuses={props.possibleStatuses}
repoCompanies={props.repoCompanies.data || []}
auctionHouses={props.auctionHouses.data || []}
onFiltersChange={postNewFilter}
/>
</div>
);
};
export default connect<IPropsFromState, IDispatchFromState, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AlertBar);
@@ -0,0 +1,110 @@
import React from "react";
import FormInput, { FormInputType } from "../../../components/form-input/FormInput";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
import { IRepoCompany } from "../../../reducers/RepoCompanyReducer";
import { IRepoAccountFilters } from "../../../services/service-types/RepoAccountTypes";
interface IAlertBarFilterDropwdownsProps {
accountFilters: IRepoAccountFilters;
statuses: string[];
repoCompanies: IRepoCompany[];
auctionHouses: IAuctionHouse[];
onFiltersChange: (filters: IRepoAccountFilters) => any;
}
const AlertBarFilterDropdowns: React.FC<IAlertBarFilterDropwdownsProps> = (props) => {
const onStatusChange = (value: string) => {
const selectValue = value !== "" ? value : undefined;
props.onFiltersChange({
...props.accountFilters,
status: selectValue
});
};
const onAuctionHouseChange = (value: string) => {
const selectValue = value !== "" ? value : undefined;
props.onFiltersChange({
...props.accountFilters,
auctionHouseId: selectValue
});
};
const onRepoIdChange = (value: string) => {
const selectValue = value !== "" ? parseInt(value) : undefined;
props.onFiltersChange({
...props.accountFilters,
repoCompanyId: selectValue
});
};
const onOnlyInlcudeOpenChange = (checked: boolean) => {
props.onFiltersChange({
...props.accountFilters,
onlyOpenAccounts: checked
})
}
return (
<div className="row">
<div className="col-md mb-3">
<FormInput
type={FormInputType.Selection}
value={props.accountFilters.status}
onChange={onStatusChange}
options={
[{ value: "", display: "All Statuses" }]
.concat(props.statuses.map((optionValue) => {
return {
value: optionValue,
display: optionValue
};
}))}
/>
</div>
<div className="col-md mb-3">
<FormInput
type={FormInputType.Selection}
value={props.accountFilters.repoCompanyId}
onChange={onRepoIdChange}
options={
[{ value: "", display: "All Repo Companies" }]
.concat(props.repoCompanies.map((repoCompany) => {
return {
value: repoCompany.id.toString(),
display: repoCompany.name
};
}))}
/>
</div>
<div className="col-md mb-3">
<FormInput
type={FormInputType.Selection}
value={props.accountFilters.auctionHouseId}
onChange={onAuctionHouseChange}
options={
[{ value: "", display: "All Auction Houses" }]
.concat(props.auctionHouses.map((auctionHouse) =>{
return {
value: auctionHouse.auctionID,
display: auctionHouse.name
};
}))}
/>
</div>
<div className="col-md mb-3 d-flex flex-row align-items-center">
<div className="px-2 pr-3 form-input-label">
<p className="mb-0">Hide Closed Accounts</p>
</div>
<FormInput
value = {props.accountFilters.onlyOpenAccounts? "true" : "false"}
type={FormInputType.Checkbox}
checked= {props.accountFilters.onlyOpenAccounts}
onChange={onOnlyInlcudeOpenChange}
/>
</div>
</div>
);
};
export default AlertBarFilterDropdowns;
@@ -0,0 +1,6 @@
.alert-bar-filter-tags .filter-tag {
font-size: 0.85em;
margin-right: 0.5em;
margin-bottom: 0.5em;
cursor: pointer;
}
@@ -0,0 +1,34 @@
import React from 'react';
import './AlertBarFilterTags.css';
export interface IAlertTag {
message: string;
onTagCleared: () => any;
}
interface IAlertBarFilterTagsProps {
filterTags: IAlertTag[];
}
const AlertBarFilterTags: React.FC<IAlertBarFilterTagsProps> = (props) => {
if (!props.filterTags.length) {
return (
<div>
<p><i>No Filters Selected.</i></p>
</div>
);
}
return (
<div className="alert-bar-filter-tags">
{props.filterTags.map((filterTag) =>
<div key={filterTag.message} className="filter-tag btn btn-primary" onClick={filterTag.onTagCleared}>
{filterTag.message}&nbsp;
<i className="fa fa-times-circle" aria-hidden="true"></i>
</div>
)}
</div>
);
};
export default AlertBarFilterTags;
@@ -0,0 +1,37 @@
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import React from "react";
import NotificationBar, {NotificationBarType} from "../../components/notification-bar/NotificationBar";
import AsyncState from "../../shared/enumerations/AsyncState";
interface IAsyncProps {
container: IAsyncStateContainer<any>;
message: string;
}
// TODO move to LCU Common, also... should use the asysnc text display...
export class AsyncContainer extends React.Component<IAsyncProps> {
private readonly showLoading: boolean;
constructor(props: any) {
super(props);
this.showLoading = (props.container.state === AsyncState.Loading);
}
public render() {
return this.showLoading ? (
<div className="mt-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Loading}
text={this.props.message}
/>
</div>
) : this.props.children;
}
}
export default AsyncContainer;
@@ -0,0 +1,4 @@
.auction-assignment-wizard .collections-history-table {
max-height: 15em;
overflow: auto;
}
@@ -0,0 +1,213 @@
import React from "react";
import { connect } from "react-redux";
import { postNewAuctionAssignment } from "../../actions/AuctionAssignmentActions";
import { getAllAuctionHouses } from "../../actions/AuctionHouseActions";
import { postSavedLocation } from "../../actions/SavedLocationActions";
import AuctionCollateralSelection, { ICollateralSelection } from "../../components/auction-collateral-selection/AuctionCollateralSelection";
import CardView from "../../components/card-view/CardView";
import NotificationBar, { NotificationBarType } from "../../components/notification-bar/NotificationBar";
import { IAuctionAssignmentPostRequest } from "../../reducers/AuctionAssignmentReducer";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import { ISavedLocation } from "../../reducers/SavedLocationReducer";
import { IBodyStyle, ICleaningLevel, ICollateralType, ICollateralTypesToVehicleType, IExternalUser, ILocationType, IVehicleType } from "../../reducers/SettingsReducer";
import RepoAccountService from "../../services/RepoAccountService";
import { ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
import { ICollateral, IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AuctionAssignmentWizardForm from "./auction-assignment-wizard-form/AuctionAssignmentWizardForm";
import "./AuctionAssignmentWizard.css";
import { getExternalUserMap } from './../../selectors/SettingsSelector';
const cloneDeep = require("lodash.clonedeep");
export interface IAuctionAssignmentWizardProps {
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
selectedAccount: IRepoAccount;
unassignedCollaterals: ICollateral[];
}
interface IPropsFromState {
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
cleaningLevels: ICleaningLevel[];
locationTypes: ILocationType[];
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
savedLocations: IAsyncStateContainer<ISavedLocation[]>;
exteralUserMap: IExternalUser[];
}
interface IPropsFromDispatch {
getAllAuctionHouses: () => any;
postAuctionAssignment: (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest) => any;
postSavedLocation: (savedLocation: ISavedLocation) => any;
}
const mapStateToProps = (appState: IAppState): IPropsFromState => {
return {
locationTypes: appState.settingsState.locationTypes,
auctionHouses: appState.auctionHouseState.auctionHouses,
cleaningLevels: appState.settingsState.cleaningLevels,
vehicleTypes: appState.settingsState.vehicleTypes,
collateralTypes: appState.settingsState.collateralTypes,
bodyStyles: appState.settingsState.bodyStyles,
collateralTypesToVehicleTypes: appState.settingsState.collateralTypesToVehicleTypes,
savedLocations: appState.savedLocationState.savedLocations,
exteralUserMap: getExternalUserMap(appState)
};
};
const mapDispatchToProps: IPropsFromDispatch = {
getAllAuctionHouses: getAllAuctionHouses,
postAuctionAssignment: postNewAuctionAssignment,
postSavedLocation: postSavedLocation
};
interface IAuctionAssignmentWizardState {
collateralSelections: ICollateralSelection[];
primaryCollateralId?: number;
}
type Props = IAuctionAssignmentWizardProps & IPropsFromState & IPropsFromDispatch;
class AuctionAssignmentWizard extends React.Component<Props, IAuctionAssignmentWizardState> {
constructor(props: Props) {
super(props);
this.state = {
collateralSelections: this.createCollateralSelections()
};
this.collateralSelectionsUpdated = this.collateralSelectionsUpdated.bind(this);
this.postAuctionAssignment = this.postAuctionAssignment.bind(this);
this.updatePrimaryCollateralId = this.updatePrimaryCollateralId.bind(this);
}
componentDidMount() {
if (this.props.auctionHouses.state === AsyncState.Inactive || this.props.auctionHouses.state === AsyncState.Failure) {
this.props.getAllAuctionHouses();
}
}
componentDidUpdate(prevPros: Props) {
// If the collateral was updated, reset the selections.
// This more than likely indicates a post request was made.
if (this.props.unassignedCollaterals !== prevPros.unassignedCollaterals) {
this.resetCollateralSelections();
}
}
resetCollateralSelections() {
this.setState({
collateralSelections: this.createCollateralSelections(),
primaryCollateralId: undefined,
});
}
createCollateralSelections() {
return this.props.unassignedCollaterals.map((collateral) => {
return {
collateral: cloneDeep(collateral),
isSelected: false,
};
});
}
collateralSelectionsUpdated(newCollateralSpecSelections: ICollateralSelection[]): void {
this.setState({
collateralSelections: newCollateralSpecSelections,
});
}
getSelectedCollaterals() {
return this.state.collateralSelections
.filter((collateralSelection) => collateralSelection.isSelected)
.map((collateralSelection) => collateralSelection.collateral);
}
updatePrimaryCollateralId(newCollateralId?: number) {
this.setState({
primaryCollateralId: newCollateralId,
});
}
postAuctionAssignment(auctionAssignmentPostRequest: IAuctionAssignmentPostRequest, shouldSaveLocation: boolean) {
this.props.postAuctionAssignment(auctionAssignmentPostRequest);
if (shouldSaveLocation) {
const vehicleLocation = auctionAssignmentPostRequest.vehicleLocation;
this.props.postSavedLocation({
name: vehicleLocation.name,
locationType: vehicleLocation.type,
address_Line_1: vehicleLocation.address,
city: vehicleLocation.city,
state: vehicleLocation.state,
zip: vehicleLocation.zip,
phone: vehicleLocation.phone
} as ISavedLocation);
}
}
render() {
const isAccountClosed = RepoAccountService.IsAccountClosed(this.props.selectedAccount);
if (!this.props.unassignedCollaterals.length) {
return (
<NotificationBar
type={NotificationBarType.Success}
text="All Collateral Assigned to an Auction"
subText="All pieces of collateral have already been assigned to an auction."
/>
);
}
if (isAccountClosed) {
return (
<NotificationBar
type={NotificationBarType.Info}
text="Account Closed"
subText="New Auctions Cannot be Created for Closed Accounts"
/>
);
}
return (
<CardView
header="Auction Assignment Creation"
rows={[
<div key="auction-assignment-creation" className="auction-assignment-wizard container">
<AuctionCollateralSelection
collateralSelections={this.state.collateralSelections}
onCollateralSelectionsChanged={this.collateralSelectionsUpdated}
vehicleTypes={this.props.vehicleTypes}
collateralTypes={this.props.collateralTypes}
bodyStyles={this.props.bodyStyles}
collateralTypesToVehicleTypes={this.props.collateralTypesToVehicleTypes}
primaryCollateralId={this.state.primaryCollateralId}
onPrimaryCollateralChanged={this.updatePrimaryCollateralId}
/>
<AuctionAssignmentWizardForm
auctionHouses={this.props.auctionHouses.data || []}
selectedAccount={this.props.selectedAccount}
cleaningLevels={this.props.cleaningLevels}
caseHistories={this.props.caseHistories}
accountHistories={this.props.accountHistories}
currentlySelectedCollaterals={this.getSelectedCollaterals()}
locationTypes={this.props.locationTypes}
savedLocations={this.props.savedLocations.data || []}
onAuctionAssignmentSubmit={this.postAuctionAssignment}
primaryCollateralId={this.state.primaryCollateralId}
externalUserMap={this.props.exteralUserMap}
/>
</div>
]}
/>
);
}
}
export const _AuctionAssignmentWizard = AuctionAssignmentWizard;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AuctionAssignmentWizard);
@@ -0,0 +1,83 @@
import React from 'react';
import { shallow } from 'enzyme';
import AuctionAssignmentWizardForm from './AuctionAssignmentWizardForm';
import AsyncState from '../../../shared/enumerations/AsyncState';
import { IAuctionAssignmentPostRequest } from '../../../reducers/AuctionAssignmentReducer';
import { IRepoAccount, ICollateral } from '../../../services/service-types/RepoAccountTypes';
{/* - 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('AuctionAssignmentWizardForm Component', () => {
it('Does Not Render on Empty Collateral', () => {
let wrapper = shallow(
<AuctionAssignmentWizardForm
auctionHouses={[]}
selectedAccount={{
accountNo: 'abcd1234',
repoId: 123
} as IRepoAccount}
cleaningLevels={[]}
currentlySelectedCollaterals={[]}
locationTypes={[]}
onAuctionAssignmentSubmit={(_) => {}}
externalUserMap={[]}
caseHistories={{state:AsyncState.Inactive}}
accountHistories={{state:AsyncState.Inactive}}
/>
);
const submitButton = wrapper.find(`[data-test='auction-assignment-wizard-form']`) ;
expect(submitButton.length).toBe(0);
})
it('Calls Submission Callback', () => {
let updatedAuctionAssignmentPostRequest: IAuctionAssignmentPostRequest | undefined;
const dummyCollateral: ICollateral = {
collateralId: 1,
repoId: 1,
vin: 'string',
make: 'string',
model: 'string',
year: 1,
repoCompanySelections: []
};
let wrapper = shallow(
<AuctionAssignmentWizardForm
auctionHouses={[]}
selectedAccount={{
accountNo: 'abcd1234',
repoId: 123
} as IRepoAccount}
cleaningLevels={[]}
caseHistories={{state:AsyncState.Inactive}}
accountHistories={{state:AsyncState.Inactive}}
currentlySelectedCollaterals={[dummyCollateral]}
locationTypes={[]}
onAuctionAssignmentSubmit={(auctionAssignmentPostRequest) => updatedAuctionAssignmentPostRequest = auctionAssignmentPostRequest}
/>
);
const submitButton = wrapper.find(`[data-test='auction-assignment-wizard-form-submit']`) ;
expect(submitButton.length).toBe(1);
submitButton.simulate('click');
expect(updatedAuctionAssignmentPostRequest).toEqual({
auctionAssignment: {
repoId: 123
},
collaterals: [dummyCollateral],
vehicleLocation: {
name: '',
phone: '',
address: '',
city: '',
state: '',
zip: '',
shouldSaveLocation: false,
},
});
})
});
*/}
@@ -0,0 +1,122 @@
import React from "react";
import AuctionAssignment, { IUserSelectedAuctionAssignment } from "../../../components/auction-assignment/AuctionAssignment";
import { IAuctionAssignment, IAuctionAssignmentPostRequest, validateAuctionAssignmentPostRequest } from "../../../reducers/AuctionAssignmentReducer";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
import { ISavedLocation } from "../../../reducers/SavedLocationReducer";
import { ICleaningLevel, IExternalUser, ILocationType } from "../../../reducers/SettingsReducer";
import { ITemenosHistory } from "../../../services/service-types/AccountHistoryTypes";
import { ICollateral, IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import HistoryTable from "../../history-table/HistoryTable";
import AuctionCollateralLocation, { ICollateralLocation } from "../../auction-collateral-location/AuctionCollateralLocation";
interface IAuctionAssignmentWizardFormProps {
auctionHouses: IAuctionHouse[];
selectedAccount: IRepoAccount;
cleaningLevels: ICleaningLevel[];
currentlySelectedCollaterals: ICollateral[];
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
locationTypes: ILocationType[];
savedLocations: ISavedLocation[];
primaryCollateralId?: number;
onAuctionAssignmentSubmit: (auctionAssignmentPotRequest: IAuctionAssignmentPostRequest, shouldSaveLocation: boolean) => any;
externalUserMap: IExternalUser[];
}
interface IAuctionAssignmentWizardFormState {
collateralLocation: ICollateralLocation;
auctionAssignment: IUserSelectedAuctionAssignment;
}
class AuctionAssignmentWizardForm extends React.Component<IAuctionAssignmentWizardFormProps, IAuctionAssignmentWizardFormState> {
constructor(props: IAuctionAssignmentWizardFormProps) {
super(props);
this.state = {
collateralLocation: {
name: "",
phone: "",
address: "",
city: "",
state: "",
zip: "",
shouldSaveLocation: false,
},
auctionAssignment: {
repoId: this.props.selectedAccount.repoId
},
};
this.collateralLocationUpdated = this.collateralLocationUpdated.bind(this);
this.auctionAssignmentUpdated = this.auctionAssignmentUpdated.bind(this);
}
collateralLocationUpdated(newCollateralLocation: ICollateralLocation) {
this.setState({
collateralLocation: newCollateralLocation,
});
}
auctionAssignmentUpdated(auctionAssignment: IUserSelectedAuctionAssignment): void {
this.setState({
auctionAssignment: auctionAssignment,
});
}
createAuctionAssignmentPostRequest(): IAuctionAssignmentPostRequest {
return {
auctionAssignment: this.state.auctionAssignment as IAuctionAssignment,
vehicleLocation: this.state.collateralLocation,
collaterals: this.props.currentlySelectedCollaterals,
primaryCollateralId: this.props.primaryCollateralId!,
};
}
render() {
if (this.props.currentlySelectedCollaterals.length) {
const auctionAssignmentPostRequest = this.createAuctionAssignmentPostRequest();
return (
<div data-test="auction-assignment-wizard-form">
<div className="aution-collateral-location">
<AuctionCollateralLocation
locationTypes={this.props.locationTypes}
savedLocations={this.props.savedLocations}
collateralLocation={this.state.collateralLocation}
onCollateralLocationChange={this.collateralLocationUpdated}
/>
</div>
<div className="collections-history-table rounded">
<HistoryTable accountNo={this.props.selectedAccount.accountNo} caseHistories={this.props.caseHistories} accountHistories={this.props.accountHistories} externalUserMap={this.props.externalUserMap}/>
</div>
<div className="auction-assignment">
<AuctionAssignment
collateralLocation={this.state.collateralLocation}
auctionHouses={this.props.auctionHouses}
auctionAssignment={this.state.auctionAssignment}
cleaningLevels={this.props.cleaningLevels}
onAuctionAssignmentChange={this.auctionAssignmentUpdated}
defaultLegalSaleDate={this.props.selectedAccount.redemptionExpiredDate}
/>
</div>
<div className="auction-assignment-actions">
<button
type="button"
className="btn btn-success"
onClick={() => this.props.onAuctionAssignmentSubmit(auctionAssignmentPostRequest, this.state.collateralLocation.shouldSaveLocation)}
disabled={!validateAuctionAssignmentPostRequest(auctionAssignmentPostRequest)}
data-test="auction-assignment-wizard-form-submit"
>
Submit Auction Assignment
</button>
</div>
</div>
);
} else {
return <div/>;
}
}
}
export default AuctionAssignmentWizardForm;
@@ -0,0 +1,3 @@
.auction-collateral-location select, .auction-collateral-location input {
margin-bottom: 0.5em;
}
@@ -0,0 +1,258 @@
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { ILocationType } from "../../reducers/SettingsReducer";
import AuctionCollateralLocation, { ICollateralLocation } from "./AuctionCollateralLocation";
describe("Auction Collateral Location Component", () => {
let wrapper: ShallowWrapper;
const dummyCollateralLocation: ICollateralLocation = {
name: "Dummy Location",
phone: "414-555-1234",
address: "1234 Main St.",
city: "Milwaukee",
state: "WI",
zip: "53207",
shouldSaveLocation: true,
};
const dummyLocationTypes: ILocationType[] = [
{
code: 1,
description: "Location Type #1",
},
{
code: 2,
description: "Location Type #2",
},
];
let dummyUpdatedCollateralLocation: ICollateralLocation | undefined;
beforeEach(() => {
wrapper = shallow(
<AuctionCollateralLocation
collateralLocation={dummyCollateralLocation}
locationTypes={dummyLocationTypes}
savedLocations={[]}
onCollateralLocationChange={(x) => dummyUpdatedCollateralLocation = x}
/>
);
});
afterEach(() => {
dummyUpdatedCollateralLocation = undefined;
});
const getCardView = () => {
return wrapper.find("CardView").dive();
};
it("Renders & Updates Location Name", () => {
// Inital value is present
let locationNameInput = getCardView().find(`[data-test='auction-collateral-location-name']`);
expect(locationNameInput.length).toBe(1);
expect(locationNameInput.props().value).toBe("Dummy Location");
// Change triggers callback
locationNameInput.simulate("change", "New Location");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.name).toEqual("New Location");
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.name).toEqual("Dummy Location");
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
name: "Update Location From Prop",
},
});
locationNameInput = getCardView().find(`[data-test='auction-collateral-location-name']`);
expect(locationNameInput.props().value).toBe("Update Location From Prop");
});
it("Renders & Updates Location Type", () => {
// Inital value is present
let locationTypeSelection = getCardView().find(`[data-test='auction-collateral-location-type']`);
expect(locationTypeSelection.length).toBe(1);
expect(locationTypeSelection.props().value).toBe(undefined);
// Change triggers callback
locationTypeSelection.simulate("change", "2");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.type).toBe(2);
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.type).toBeUndefined();
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
type: 1,
},
});
locationTypeSelection = getCardView().find(`[data-test='auction-collateral-location-type']`);
expect(locationTypeSelection.props().value).toBe(1);
});
it("Renders & Updates Phone", () => {
// Inital value is present
let locationPhoneInput = getCardView().find(`[data-test='auction-collateral-location-phone']`);
expect(locationPhoneInput.length).toBe(1);
expect(locationPhoneInput.props().value).toBe("414-555-1234");
// Change triggers callback
locationPhoneInput.simulate("change", "414-555-4321");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.phone).toEqual("414-555-4321");
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.phone).toEqual("414-555-1234");
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
phone: "414-555-0001",
},
});
locationPhoneInput = getCardView().find(`[data-test='auction-collateral-location-phone']`);
expect(locationPhoneInput.props().value).toBe("414-555-0001");
});
it("Renders & Updates Street", () => {
// Inital value is present
let locationStreetInput = getCardView().find(`[data-test='auction-collateral-location-street']`);
expect(locationStreetInput.length).toBe(1);
expect(locationStreetInput.props().value).toBe("1234 Main St.");
// Change triggers callback
locationStreetInput.simulate("change", "1600 Pennsylvania Ave NW");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.address).toEqual("1600 Pennsylvania Ave NW");
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.address).toEqual("1234 Main St.");
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
address: "Westminster",
},
});
locationStreetInput = getCardView().find(`[data-test='auction-collateral-location-street']`);
expect(locationStreetInput.props().value).toBe("Westminster");
});
it("Renders & Updates Street", () => {
// Inital value is present
let locationCityInput = getCardView().find(`[data-test='auction-collateral-location-city']`);
expect(locationCityInput.length).toBe(1);
expect(locationCityInput.props().value).toBe("Milwaukee");
// Change triggers callback
locationCityInput.simulate("change", "Waukesha");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.city).toEqual("Waukesha");
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.city).toEqual("Milwaukee");
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
city: "Madison",
},
});
locationCityInput = getCardView().find(`[data-test='auction-collateral-location-city']`);
expect(locationCityInput.props().value).toBe("Madison");
});
it("Renders & Updates State", () => {
// Inital value is present
let locationStateInput = getCardView().find(`[data-test='auction-collateral-location-state']`);
expect(locationStateInput.length).toBe(1);
expect(locationStateInput.props().value).toBe("WI");
// Change triggers callback
locationStateInput.simulate("change", "GA");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.state).toEqual("GA");
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.state).toEqual("WI");
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
state: "IL",
},
});
locationStateInput = getCardView().find(`[data-test='auction-collateral-location-state']`);
expect(locationStateInput.props().value).toBe("IL");
});
it("Renders & Updates ZIP", () => {
// Inital value is present
let locationZipInput = getCardView().find(`[data-test='auction-collateral-location-zip']`);
expect(locationZipInput.length).toBe(1);
expect(locationZipInput.props().value).toBe("53207");
// Change triggers callback
locationZipInput.simulate("change", "53211");
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.zip).toEqual("53211");
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.zip).toEqual("53207");
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
zip: "53202",
},
});
locationZipInput = getCardView().find(`[data-test='auction-collateral-location-zip']`);
expect(locationZipInput.props().value).toBe("53202");
});
it("Renders & Updates Save Location", () => {
// Inital value is present
let locationSaveCheckbox = getCardView().find(`[data-test='auction-collateral-location-should-save']`);
expect(locationSaveCheckbox.length).toBe(1);
expect(locationSaveCheckbox.props().checked).toBe(true);
// Change triggers callback
locationSaveCheckbox.simulate("change", {target: {checked: false}});
expect(dummyUpdatedCollateralLocation).not.toBeUndefined();
expect(dummyUpdatedCollateralLocation!.shouldSaveLocation).toEqual(false);
// Change does not alter original prop object
expect(dummyCollateralLocation).not.toBe(dummyUpdatedCollateralLocation);
expect(dummyCollateralLocation.shouldSaveLocation).toEqual(true);
// Prop changes update input
wrapper = wrapper.setProps({
collateralLocation: {
...dummyCollateralLocation,
shouldSaveLocation: false,
},
});
locationSaveCheckbox = getCardView().find(`[data-test='auction-collateral-location-should-save']`);
expect(locationSaveCheckbox.props().checked).toBe(false);
});
});
@@ -0,0 +1,207 @@
import React from "react";
import CardView from "../../components/card-view/CardView";
import FormInput, { FormInputType } from "../../components/form-input/FormInput";
import { IVehicleLocation } from "../../reducers/AuctionAssignmentReducer";
import { ISavedLocation } from "../../reducers/SavedLocationReducer";
import { ILocationType } from "../../reducers/SettingsReducer";
import "./AuctionCollateralLocation.css";
const cloneDeep = require("lodash.clonedeep");
export interface ICollateralLocation extends IVehicleLocation {
shouldSaveLocation: boolean;
}
export interface IAuctionCollateralLocationProps {
collateralLocation: ICollateralLocation;
locationTypes: ILocationType[];
savedLocations: ISavedLocation[];
onCollateralLocationChange: (cl: ICollateralLocation) => any;
}
interface IAuctionCollateralLocationState {
selectedLocation?: ISavedLocation;
}
type ICollateralLocationUpdater = (x: ICollateralLocation) => void;
class AuctionCollateralLocation extends React.Component<IAuctionCollateralLocationProps, IAuctionCollateralLocationState> {
constructor(props: IAuctionCollateralLocationProps) {
super(props);
this.state = {};
this.applyChange = this.applyChange.bind(this);
this.handleShouldSaveLocation = this.handleShouldSaveLocation.bind(this);
this.handleSavedLocationSelection = this.handleSavedLocationSelection.bind(this);
}
componentDidUpdate(_: IAuctionCollateralLocationProps, prevState: IAuctionCollateralLocationState) {
if (
prevState.selectedLocation !== this.state.selectedLocation &&
this.state.selectedLocation
) {
this.applyChange((collateralLocation) => {
collateralLocation.name = this.state.selectedLocation!.name;
collateralLocation.address = this.state.selectedLocation!.address_Line_1;
collateralLocation.type = this.state.selectedLocation!.locationType;
collateralLocation.phone = this.state.selectedLocation!.phone || "";
collateralLocation.city = this.state.selectedLocation!.city;
collateralLocation.state = this.state.selectedLocation!.state;
collateralLocation.zip = this.state.selectedLocation!.zip;
collateralLocation.shouldSaveLocation = false;
});
}
}
applyChange(updater: ICollateralLocationUpdater) {
const collateralLocationCopy = cloneDeep(this.props.collateralLocation);
updater(collateralLocationCopy);
this.props.onCollateralLocationChange(collateralLocationCopy);
}
applyLocationInputChange(updater: ICollateralLocationUpdater) {
this.applyChange(updater);
this.clearLocationSelection();
}
handleShouldSaveLocation(event: React.ChangeEvent<HTMLInputElement>) {
this.applyChange((collateralLocation) => collateralLocation.shouldSaveLocation = event.target.checked);
}
handleSavedLocationSelection(selectionValue: string): void {
if (selectionValue === "") {
this.clearLocationSelection();
return;
}
const savedLocationId = parseInt(selectionValue);
const matchingLocation = this.props.savedLocations.find(savedLocation => savedLocation.id === savedLocationId);
this.setState({
selectedLocation: matchingLocation
});
}
clearLocationSelection(): void {
this.setState({
selectedLocation: undefined
});
}
render() {
return (
<CardView
header="Collateral Location"
rows={[
<div key="collateral-location" className="auction-collateral-location">
<div className="row">
<div className="col">
<FormInput
label="Saved Location"
type={FormInputType.Selection}
value={this.state.selectedLocation ? this.state.selectedLocation.id : undefined}
onChange={this.handleSavedLocationSelection}
placeholder="Saved Locations..."
options={this.props.savedLocations.map((savedLocation) => {
return {
value: savedLocation.id,
display: savedLocation.name
};
})}
data-test="auction-collateral-saved-locations"
/>
<FormInput
label="Location Name"
required={true}
type={FormInputType.Text}
value={this.props.collateralLocation.name}
onChange={(locationName) => this.applyLocationInputChange(collateralLocation => collateralLocation.name = locationName)}
placeholder="Location Name"
data-test="auction-collateral-location-name"
/>
<FormInput
label="Location Type"
required={true}
type={FormInputType.Selection}
value={this.props.collateralLocation.type}
onChange={(locationType) => this.applyLocationInputChange(collateralLocation =>
collateralLocation.type = locationType === "" ? undefined : parseInt(locationType))}
placeholder="Location Type..."
options={this.props.locationTypes.map((locationType) => {
return {
value: locationType.code,
display: locationType.description
};
})}
data-test="auction-collateral-location-type"
/>
<FormInput
label="Phone"
type={FormInputType.Text}
value={this.props.collateralLocation.phone}
onChange={(phone) => this.applyLocationInputChange(collateralLocation => collateralLocation.phone = phone)}
placeholder="Phone"
data-test="auction-collateral-location-phone"
/>
</div>
<div className="col">
<FormInput
label="Street"
required={true}
type={FormInputType.Text}
value={this.props.collateralLocation.address}
onChange={(address) => this.applyLocationInputChange(collateralLocation => collateralLocation.address = address)}
placeholder="Street"
data-test="auction-collateral-location-street"
/>
<FormInput
label="City"
required={true}
type={FormInputType.Text}
value={this.props.collateralLocation.city}
onChange={(city) => this.applyLocationInputChange(collateralLocation => collateralLocation.city = city)}
placeholder="City"
data-test="auction-collateral-location-city"
/>
<FormInput
label="State"
required={true}
type={FormInputType.Text}
value={this.props.collateralLocation.state}
onChange={(state) => this.applyLocationInputChange(collateralLocation => collateralLocation.state = state)}
placeholder="State"
data-test="auction-collateral-location-state"
/>
<FormInput
label="Zip Code"
required={true}
type={FormInputType.Text}
value={this.props.collateralLocation.zip}
onChange={(zip) => this.applyLocationInputChange(collateralLocation => collateralLocation.zip = zip)}
placeholder="Zip Code"
data-test="auction-collateral-location-zip"
/>
</div>
</div>
<div className="form-check">
<input
type="checkbox"
className="form-check-input"
disabled={!!this.state.selectedLocation}
checked={this.props.collateralLocation.shouldSaveLocation}
onChange={this.handleShouldSaveLocation}
data-test="auction-collateral-location-should-save"
/>
<label className="form-check-label">Save Location</label>
</div>
</div>
]}
/>
);
}
}
export default AuctionCollateralLocation;
@@ -0,0 +1,40 @@
.auction-info-tab p {
margin-bottom: 0;
}
.auction-info-tab h5 {
padding-top: 0.25em;
}
.auction-info-tab .alert {
margin-top: 1em;
}
.auction-info-tab .auto-ims-image {
max-height: 225px;
}
.auction-info-tab .auto-ims-img-col a {
display: block;
text-align: center;
}
.auction-info-tab .autoims-general-info hr {
margin: 0.5em 67% 0.5em 0em;
}
.auction-info-tab .autoims-general-info .row {
padding-bottom: 1em;
}
.auction-info-tab .collateral-section .row:not(:last-child) {
padding-bottom: 0;
}
.auction-info-tab .collateral-row hr {
margin: 0.5em 33% 0.5em 0em;
}
.auction-info-tab .auction-panel-btn {
width: 100%;
}
@@ -0,0 +1,36 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { _AuctionInfo } from './AuctionInfo';
import { IRepoAccount } from '../../services/service-types/RepoAccountTypes';
import { IAuctionAssignment, IAuctionAssignmentToCollateral } from '../../reducers/AuctionAssignmentReducer';
describe('AuctionInfoTab component', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<_AuctionInfo
selectedAccount={{} as IRepoAccount}
loadAuctionAssignment={() => {}}
auctionAssignments={[
{
auctionAssignmentToCollaterals: [] as IAuctionAssignmentToCollateral[]
} as IAuctionAssignment,
{
auctionAssignmentToCollaterals: [] as IAuctionAssignmentToCollateral[]
} as IAuctionAssignment
]}
putRepoAccount={() => {}}
putAuctionAssignment={() => {}}
holdStatuses={[]}
removeAuctionAssignment={() => {}}
/>
);
});
it ('Renders Proper number of Auction Assignments Assignment', () => {
const auctionInfoAssignment = wrapper.find(`[data-test='auction-info-assignment']`);
expect(auctionInfoAssignment.length).toBe(2);
});
});
@@ -0,0 +1,101 @@
import React from "react";
import { connect } from "react-redux";
import { fetchAuctionAssignment, putAuctionAssignment, removeAuction } from "../../actions/AuctionAssignmentActions";
import { putRepoAccount } from "../../actions/RepoAccountActions";
import { IAuctionAssignment } from "../../reducers/AuctionAssignmentReducer";
import { IHoldStatus } from "../../reducers/SettingsReducer";
import { ICollateral, IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import { IAppState } from "../../store/Store";
import AuctionInfoAssignment from "./auction-info-assignment/AuctionInfoAssignment";
import "./AuctionInfo.css";
export interface IAuctionInfoProps {
readonly selectedAccount: IRepoAccount;
auctionAssignments: IAuctionAssignment[];
}
interface IPropsFromState {
readonly holdStatuses: IHoldStatus[];
}
interface IPropsFromDispatch {
readonly loadAuctionAssignment: (id: number) => any;
readonly putAuctionAssignment: (auctionAssignment: IAuctionAssignment) => any;
readonly putRepoAccount: (repoAccount: IRepoAccount) => any;
readonly removeAuctionAssignment: (auctionAssignmentId: number) => any;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
holdStatuses: state.settingsState.holdStatuses,
};
};
const mapDispatchToProps: IPropsFromDispatch = {
loadAuctionAssignment: fetchAuctionAssignment,
putRepoAccount: putRepoAccount,
putAuctionAssignment: putAuctionAssignment,
removeAuctionAssignment: removeAuction
};
type Props = IPropsFromState & IPropsFromDispatch & IAuctionInfoProps;
const AuctionInfo: React.FC<Props> = (props) => {
const getCollateralFromAuctionAssignment = (auctionAssignment: IAuctionAssignment): ICollateral[] => {
return auctionAssignment.auctionAssignmentToCollaterals.map(aa2c => aa2c.collateral);
};
const removeAutionAssignment = (auctionAssignment: IAuctionAssignment): void => {
const answer = window.confirm("Are you sure you want to remove this auction?");
if (answer) {
props.removeAuctionAssignment(auctionAssignment.id);
}
};
const createSortedAuctionAssignmentArray = (): IAuctionAssignment[] => {
return props.auctionAssignments
.concat()
.sort((a, b) => {
if (a.dateSubmitted && b.dateSubmitted) {
return -(a.dateSubmitted.unix() - b.dateSubmitted.unix());
}
if (a.dateSubmitted) {
return 1;
}
return -1;
})
.sort((a, b) => {
if (a.isActive && !b.isActive) {
return -1;
}
if (!a.isActive && b.isActive) {
return 1;
}
return 0;
});
};
const sortedAuctionAssignments = createSortedAuctionAssignmentArray();
return (
<div className="auction-info-tab">
{sortedAuctionAssignments.map((auctionAssignment) =>
<AuctionInfoAssignment
key={auctionAssignment.auctionId}
selectedAccount={props.selectedAccount}
auctionAssignment={auctionAssignment}
holdStatuses={props.holdStatuses}
putRepoAccount={props.putRepoAccount}
putAuctionAssignment={props.putAuctionAssignment}
collaterals={getCollateralFromAuctionAssignment(auctionAssignment)}
onAuctionAssignmentRemoved={removeAutionAssignment}
data-test="auction-info-assignment"
/>
)}
</div>
);
};
export const _AuctionInfo = AuctionInfo;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AuctionInfo);
@@ -0,0 +1,115 @@
import React from 'react';
import { IAuctionAssignment } from '../../../reducers/AuctionAssignmentReducer';
import { IHoldStatus } from '../../../reducers/SettingsReducer';
import AuctionSoldBanner from './auction-sold-banner/AuctionSoldBanner';
import { IRepoAccount, ICollateral } from '../../../services/service-types/RepoAccountTypes';
import NotificationBar, { NotificationBarType } from '../../../components/notification-bar/NotificationBar';
import AutoIMSDisplay from './auto-ims-display/AutoIMSDisplay';
import RepoAccountService from '../../../services/RepoAccountService';
import AuctionInfoSelectionHeader from './auction-info-selection-header/AuctionInfoSelectionHeader';
interface IAuctionInfoAssignmentProps {
auctionAssignment: IAuctionAssignment;
selectedAccount: IRepoAccount;
holdStatuses: IHoldStatus[];
collaterals: ICollateral[];
putAuctionAssignment: (auctionAssignment: IAuctionAssignment) => any;
putRepoAccount: (repoAccount: IRepoAccount) => any;
onAuctionAssignmentRemoved: (auctionAssignment: IAuctionAssignment) => any;
}
interface IAuctionInfoAssignmentState {
isAutoIMSDisplayOpen: boolean;
}
class AuctionInfoAssignment extends React.Component<IAuctionInfoAssignmentProps, IAuctionInfoAssignmentState> {
constructor(props: IAuctionInfoAssignmentProps) {
super(props);
this.state = {
isAutoIMSDisplayOpen: props.auctionAssignment.isActive
}
this.toggleAutoIMSAccordion = this.toggleAutoIMSAccordion.bind(this);
}
toggleAutoIMSAccordion(): void {
this.setState({
isAutoIMSDisplayOpen: !this.state.isAutoIMSDisplayOpen
});
}
isAuctionSold(auctionAssignment: IAuctionAssignment): boolean {
if (!auctionAssignment.autoIMS) {
return false;
}
return !!auctionAssignment.autoIMS.soldDate && !!auctionAssignment.autoIMS.salePrice && auctionAssignment.autoIMS.salePrice > 0;
}
render() {
const isAccountClosed = RepoAccountService.IsAccountClosed(this.props.selectedAccount);
const auctionAssignment = this.props.auctionAssignment;
const auction = this.props.auctionAssignment.auction;
const autoIMS = this.props.auctionAssignment.autoIMS;
if (!auctionAssignment.autoIMS) {
if (this.props.auctionAssignment.isActive) {
return (
<NotificationBar
key={auctionAssignment.id}
type={NotificationBarType.Info}
text={`Auction Assignment created ${auctionAssignment.auction ? 'at ' + auctionAssignment.auction.name : '' }. AutoIMS pending.`}
isButtonPresent={true}
buttonText="Remove From Auction"
onButtonClick={() => this.props.onAuctionAssignmentRemoved(auctionAssignment)}
data-test="no-autoims-alert"
/>
)
} else {
return (
<NotificationBar
key={auctionAssignment.id}
type={NotificationBarType.Warning}
text={`Auction Assignment cancelled ${auctionAssignment.auction ? 'at ' + auctionAssignment.auction.name : '' }. AutoIMS info not present.`}
data-test="no-autoims-alert"
/>
)
}
}
const isAuctionCancelled = !auctionAssignment.isActive;
const isAuctionSold = this.isAuctionSold(this.props.auctionAssignment);
return (
<div className="auction-info-assignment">
<AuctionSoldBanner autoIMS={autoIMS} />
<div className="card card-view-main">
<AuctionInfoSelectionHeader
isOpen={this.state.isAutoIMSDisplayOpen}
isAuctionSold={isAuctionSold}
isAuctionCancelled={isAuctionCancelled}
auctionHouse={auction}
onHeaderClicked={this.toggleAutoIMSAccordion}
/>
{ this.state.isAutoIMSDisplayOpen ?
<ul className="list-group list-group-flush">
<li className="list-group-item">
<AutoIMSDisplay
auctionAssignment={auctionAssignment}
selectedAccount={this.props.selectedAccount}
putAuctionAssignment={this.props.putAuctionAssignment}
putRepoAccount={this.props.putRepoAccount}
holdStatuses={this.props.holdStatuses}
collaterals={this.props.collaterals}
onAuctionAssignmentRemoved={this.props.onAuctionAssignmentRemoved}
isAuctionLocked={isAuctionCancelled || isAccountClosed || isAuctionSold}
/>
</li>
</ul>
: null}
</div>
</div>
);
}
}
export default AuctionInfoAssignment;
@@ -0,0 +1,52 @@
import React from 'react';
import { shallow } from 'enzyme';
import AnnouncedConditions from './AnnouncedConditions';
describe('AnnouncedConditions Component', () => {
it ('Renders Appropriate Text When Announced Conditions is Undefined', () => {
const wrapper = shallow(<AnnouncedConditions />);
const noAnnouncedConditiosLabel = wrapper.find(`[data-test='announced-conditions-none']`);
expect(noAnnouncedConditiosLabel.length).toBe(1);
});
it ('Renders Appropriate Text When Announced Conditions is Empty String', () => {
const wrapper = shallow(<AnnouncedConditions announcedConditions="" />);
const noAnnouncedConditiosLabel = wrapper.find(`[data-test='announced-conditions-none']`);
expect(noAnnouncedConditiosLabel.length).toBe(1);
});
it ('Renders Only Left Bullets When One Announced Condition (#1)', () => {
const wrapper = shallow(<AnnouncedConditions announcedConditions=" bullet1 ; " />);
const leftBullets = wrapper.find(`[data-test='announced-condition-left-bullet']`);
expect(leftBullets.length).toBe(1);
const rightBullets = wrapper.find(`[data-test='announced-condition-right-bullet']`);
expect(rightBullets.length).toBe(0);
expect(leftBullets.text()).toEqual('bullet1');
});
it ('Renders Only Left Bullets When One Announced Condition (#2)', () => {
const wrapper = shallow(<AnnouncedConditions announcedConditions=" bullet1 " />);
const leftBullets = wrapper.find(`[data-test='announced-condition-left-bullet']`);
expect(leftBullets.length).toBe(1);
const rightBullets = wrapper.find(`[data-test='announced-condition-right-bullet']`);
expect(rightBullets.length).toBe(0);
expect(leftBullets.text()).toEqual('bullet1');
});
it ('Renders Multiple Bullets When Multiple Announced Conditions', () => {
const wrapper = shallow(<AnnouncedConditions announcedConditions="bullet1;bullet2 ; bullet3;bullet4 ; bullet5; " />);
const leftBullets = wrapper.find(`[data-test='announced-condition-left-bullet']`);
expect(leftBullets.length).toBe(3);
const rightBullets = wrapper.find(`[data-test='announced-condition-right-bullet']`);
expect(rightBullets.length).toBe(2);
expect(leftBullets.at(0).text()).toEqual('bullet1');
expect(leftBullets.at(1).text()).toEqual('bullet3');
expect(leftBullets.at(2).text()).toEqual('bullet5');
expect(rightBullets.at(0).text()).toEqual('bullet2');
expect(rightBullets.at(1).text()).toEqual('bullet4');
});
});
@@ -0,0 +1,61 @@
import React from 'react';
interface IAnnouncedConditions {
announcedConditions?: string;
}
const AnnouncedConditions: React.FC<IAnnouncedConditions> = (props) => {
const areAnnouncedConditionsPresent = (): boolean => {
return props.announcedConditions !== undefined && props.announcedConditions.trim() !== '';
}
const getFormattedAnnouncedConditions = (announcedConditions: string): string[] => {
return announcedConditions
.split(';')
.map((condition) => condition.trim())
.filter((condition) => condition !== '');
};
const getLeftBullets = (announcedConditions: string): string[] => {
return getFormattedAnnouncedConditions(announcedConditions).filter((_, i) => i % 2 === 0);
}
const getRightBullets = (announcedConditions: string): string[] => {
return getFormattedAnnouncedConditions(announcedConditions).filter((_, i) => i % 2 !== 0);
}
return (
<div>
<h5>Announced Conditions</h5>
<hr></hr>
{
!areAnnouncedConditionsPresent()
?
<div className="col">
<p><i data-test="announced-conditions-none">No Announced Conditions</i></p>
</div>
:
<div>
<div className="col-sm">
<ul>
{getLeftBullets(props.announcedConditions!).map((conditionText, index) =>
<li key={index} data-test="announced-condition-left-bullet">{conditionText}</li>
)}
</ul>
</div>
<div className="col-sm">
<ul>
{getRightBullets(props.announcedConditions!).map((conditionText, index) =>
<li key={index} data-test="announced-condition-right-bullet">{conditionText}</li>
)}
</ul>
</div>
</div>
}
</div>
);
};
export default AnnouncedConditions;
@@ -0,0 +1,46 @@
import React from 'react';
import { shallow } from 'enzyme';
import AuctionCollaterals from './AuctionCollaterals';
import { ICollateral } from '../../../../reducers/RepoAccountReducer';
describe('AuctionCollaterals Component', () => {
it ('Renders Empty Collaterals Properly', () => {
const wrapper = shallow(<AuctionCollaterals collaterals={[]} />);
const collateralType = wrapper.find(`[data-test='auction-collaterals-type']`);
const collateralVin = wrapper.find(`[data-test='auction-collaterals-vin']`);
expect(collateralType.length).toBe(0);
expect(collateralVin.length).toBe(0);
});
it ('Renders Non-Empty Collaterals Properly', () => {
const wrapper = shallow(<AuctionCollaterals collaterals={[
{
collateralId: 1,
year: 2012,
make: 'Chevy',
model: 'Cruze',
vin: 'abcd1234'
} as ICollateral,
{
collateralId: 2,
year: 1914,
make: 'Ford',
model: 'Focus',
vin: '1234abcd'
} as ICollateral,
]} />);
const collateralType = wrapper.find(`[data-test='auction-collaterals-type']`);
const collateralVin = wrapper.find(`[data-test='auction-collaterals-vin']`);
expect(collateralType.length).toBe(2);
expect(collateralVin.length).toBe(2);
expect(collateralType.at(0).text()).toEqual('2012 Chevy Cruze');
expect(collateralType.at(1).text()).toEqual('1914 Ford Focus');
expect(collateralVin.at(0).text()).toEqual('abcd1234');
expect(collateralVin.at(1).text()).toEqual('1234abcd');
});
});
@@ -0,0 +1,40 @@
import React from 'react';
import { ICollateral } from '../../../../services/service-types/RepoAccountTypes';
interface IAuctionCollateralsProps {
collaterals: ICollateral[];
}
const AuctionCollaterals: React.FC<IAuctionCollateralsProps> = (props) => {
return (
<div className="auction-collaterals">
<h5>Collateral</h5>
<hr></hr>
{props.collaterals.map((collateral, index) =>
<div key={collateral.collateralId} className="collateral-row" data-test="collateral-auction-assignment-row">
<div className="row">
<div className="col">
<p>
<b>Type: </b>
<span data-test="auction-collaterals-type">
{`${collateral.year} ${collateral.make} ${collateral.model}`}
</span>
</p>
</div>
<div className="col">
<p>
<b>Vin: </b>
<span data-test="auction-collaterals-vin">
{collateral.vin}
</span>
</p>
</div>
</div>
{ index < props.collaterals.length - 1 ? <hr></hr> : undefined}
</div>
)}
</div>
);
};
export default AuctionCollaterals;
@@ -0,0 +1,90 @@
import React from 'react';
import { shallow } from 'enzyme';
import AuctionHouseInfo from './AuctionHouseInfo';
import { IAuctionHouse } from '../../../../reducers/AuctionHouseReducer';
describe('AuctionHouseInfo Component', () => {
it ('Renders Error Notification When Auction House is Undefined', () => {
const wrapper = shallow(<AuctionHouseInfo />);
const auctionHosueMissing = wrapper.find(`[data-test='auction-house-info-missing']`);
expect(auctionHosueMissing.length).toBe(1);
});
it ('Renders Addresses Properly (#1)', () => {
const wrapper = shallow(
<AuctionHouseInfo
auctionHouse={{
address_Line_1: ' 1234 S. Main Street ',
address_Line_2: '',
city: 'Milwaukee',
state: 'Wisconsin',
zip: 10001,
country: 'USA',
phone: '414-555-1234',
} as IAuctionHouse}
/>
);
const auctionHouseAddress = wrapper.find(`[data-test='auction-house-info-address']`);
expect(auctionHouseAddress.length).toBe(1);
expect(auctionHouseAddress.text()).toEqual('1234 S. Main Street, Milwaukee, Wisconsin 10001, USA');
});
it ('Renders Addresses Properly (#2)', () => {
const wrapper = shallow(
<AuctionHouseInfo
auctionHouse={{
address_Line_1: ' 1234 S. Main Street ',
address_Line_2: '-',
city: 'Milwaukee',
state: 'Wisconsin',
zip: 10001,
country: 'USA',
phone: '414-555-1234',
} as IAuctionHouse}
/>
);
const auctionHouseAddress = wrapper.find(`[data-test='auction-house-info-address']`);
expect(auctionHouseAddress.length).toBe(1);
expect(auctionHouseAddress.text()).toEqual('1234 S. Main Street, Milwaukee, Wisconsin 10001, USA');
});
it ('Renders Addresses Properly (#3)', () => {
const wrapper = shallow(
<AuctionHouseInfo
auctionHouse={{
address_Line_1: ' 1234 S. Main Street ',
address_Line_2: 'P.O. #123',
city: 'Milwaukee',
state: 'Wisconsin',
zip: 10001,
country: 'USA',
phone: '414-555-1234',
} as IAuctionHouse}
/>
);
const auctionHouseAddress = wrapper.find(`[data-test='auction-house-info-address']`);
expect(auctionHouseAddress.length).toBe(1);
expect(auctionHouseAddress.text()).toEqual('1234 S. Main Street P.O. #123, Milwaukee, Wisconsin 10001, USA');
});
it ('Renders Phone Number', () => {
const wrapper = shallow(
<AuctionHouseInfo
auctionHouse={{
address_Line_1: ' 1234 S. Main Street ',
address_Line_2: 'P.O. #123',
city: 'Milwaukee',
state: 'Wisconsin',
zip: 10001,
country: 'USA',
phone: '414-555-1234',
} as IAuctionHouse}
/>
);
const auctionHousePhone = wrapper.find(`[data-test='auction-house-info-phone']`);
expect(auctionHousePhone.length).toBe(1);
expect(auctionHousePhone.text()).toEqual('414-555-1234');
});
});
@@ -0,0 +1,47 @@
import React from 'react';
import { IAuctionHouse } from '../../../../reducers/AuctionHouseReducer';
import NotificationBar, { NotificationBarType } from '../../../../components/notification-bar/NotificationBar';
interface IAuctionHouseInfoProps {
auctionHouse?: IAuctionHouse;
}
const AuctionHouseInfo: React.FC<IAuctionHouseInfoProps> = (props) => {
const getCombinedAddressLines = (auctionHouse: IAuctionHouse): string => {
let addressLinesCombined = auctionHouse.address_Line_1.trim();
const addressLine2 = auctionHouse.address_Line_2 ? auctionHouse.address_Line_2.trim() : '';
if (addressLine2 !== '' && addressLine2 !== '-') {
addressLinesCombined += ' ' + addressLine2;
}
return addressLinesCombined;
}
const auctionHouse = props.auctionHouse;
return (
<div className="auction-house-info">
<h5>Auction House</h5>
<hr></hr>
{
!auctionHouse
?
<NotificationBar
type={NotificationBarType.Warning}
text="Auction House Data Missing"
data-test="auction-house-info-missing" />
:
<div className="row">
<div className="col">
<p data-test="auction-house-info-address">
{`${getCombinedAddressLines(auctionHouse)}, ${auctionHouse.city}, ${auctionHouse.state} ${auctionHouse.zip}, ${auctionHouse.country}`}
</p>
<p data-test="auction-house-info-phone">{auctionHouse.phone}</p>
</div>
</div>
}
</div>
);
};
export default AuctionHouseInfo;
@@ -0,0 +1,7 @@
.auction-info-assignment-header {
cursor: pointer;
}
.auction-info-assignment-header .fa {
margin-right: 0.5em;
}
@@ -0,0 +1,94 @@
import React from 'react';
import { ShallowWrapper, shallow } from 'enzyme';
import AuctionInfoSelectionHeader from './AuctionInfoSelectionHeader';
import { IAuctionHouse } from '../../../../reducers/AuctionHouseReducer';
describe('AuctionInfoSelectionHeader Component', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow((
<AuctionInfoSelectionHeader
isOpen={true}
isAuctionCancelled={false}
isAuctionSold={false}
auctionHouse={{ name: 'ABC Auction Co.' } as IAuctionHouse}
onHeaderClicked={() => {}}
/>
));
});
it ('Renders Active Auction Styles', () => {
let headerIcon = wrapper.find(`[data-test='header-font-awesome-icon']`);
expect(headerIcon.hasClass('fa-clock-o')).toBeTruthy();
let postTitleText = wrapper.find(`[data-test='post-title-text']`);
expect(postTitleText.text()).toEqual('');
let parentHeader = wrapper.find(`[data-test='auction-info-selection-main-header']`);
expect(parentHeader.hasClass('bg-secondary')).toBeTruthy();
});
it ('Renders Inactive Auction Styles', () => {
wrapper = wrapper.setProps({
isAuctionCancelled: true
});
let headerIcon = wrapper.find(`[data-test='header-font-awesome-icon']`);
expect(headerIcon.hasClass('fa-exclamation-triangle')).toBeTruthy();
let postTitleText = wrapper.find(`[data-test='post-title-text']`);
expect(postTitleText.text()).toContain('(Auction Cancelled)');
let parentHeader = wrapper.find(`[data-test='auction-info-selection-main-header']`);
expect(parentHeader.hasClass('bg-warning')).toBeTruthy();
});
it ('Renders Sold Auction Styles', () => {
wrapper = wrapper.setProps({
isAuctionSold: true
});
let headerIcon = wrapper.find(`[data-test='header-font-awesome-icon']`);
expect(headerIcon.hasClass('fa-check-circle')).toBeTruthy();
let postTitleText = wrapper.find(`[data-test='post-title-text']`);
expect(postTitleText.text()).toContain('(Auction Sold)');
let parentHeader = wrapper.find(`[data-test='auction-info-selection-main-header']`);
expect(parentHeader.hasClass('bg-success')).toBeTruthy();
});
it ('Renders Auction House Name', () => {
const auctionHouseName = wrapper.find(`[data-test='auction-house-name']`);
expect(auctionHouseName.text()).toEqual('ABC Auction Co.');
});
it ('Renders Chevron Down When Open', () => {
const chevronIcon = wrapper.find(`[data-test='chevron-icon']`);
expect(chevronIcon.hasClass('fa-chevron-down')).toBeTruthy();
});
it ('Renders Chevron Up When Closed', () => {
wrapper = wrapper.setProps({
isOpen: false
});
const chevronIcon = wrapper.find(`[data-test='chevron-icon']`);
expect(chevronIcon.hasClass('fa-chevron-up')).toBeTruthy();
});
it ('Calls onHeaderClicked When Clicked', () => {
let wasCallbackTriggered = false;
wrapper = wrapper.setProps({
onHeaderClicked: () => { wasCallbackTriggered = true }
});
let parentHeader = wrapper.find(`[data-test='auction-info-selection-main-header']`);
parentHeader.simulate('click');
expect(wasCallbackTriggered).toBe(true);
});
})
@@ -0,0 +1,77 @@
import React from 'react';
import { IAuctionHouse } from '../../../../reducers/AuctionHouseReducer';
import './AuctionInfoSelectionHeader.css';
interface IAuctionInfoSelectionHeaderProps {
isOpen: boolean;
isAuctionCancelled: boolean;
isAuctionSold: boolean;
auctionHouse?: IAuctionHouse;
onHeaderClicked: () => any;
}
const getAuctionHeaderBgClass = (isAuctionCancelled: boolean, isAuctionSold: boolean): string => {
if (isAuctionSold) {
return 'bg-success';
}
if (isAuctionCancelled) {
return 'bg-warning';
}
return 'bg-secondary';
}
const getAuctionHeaderColorClass = (isAuctionCancelled: boolean, isAuctionSold: boolean): string => {
if (isAuctionSold || !isAuctionCancelled) {
return 'text-white';
}
return '';
}
const getFontAwesomeClass = (isAuctionCancelled: boolean, isAuctionSold: boolean): string => {
if (isAuctionCancelled) {
return 'fa-exclamation-triangle';
}
if (isAuctionSold) {
return 'fa-check-circle';
}
return 'fa-clock-o';
}
const getPostTitleText = (isAuctionCancelled: boolean, isAuctionSold: boolean): string => {
if (isAuctionCancelled) {
return ' (Auction Cancelled)';
}
if (isAuctionSold) {
return ' (Auction Sold)';
}
return '';
}
const AuctionInfoSelectionHeader: React.FC<IAuctionInfoSelectionHeaderProps> = (props) => {
return (
<div
className={'auction-info-assignment-header card-body ' + getAuctionHeaderBgClass(props.isAuctionCancelled, props.isAuctionSold)}
onClick={props.onHeaderClicked}
data-test="auction-info-selection-main-header"
>
<div className="row">
<div className="col">
<h6 className={getAuctionHeaderColorClass(props.isAuctionCancelled, props.isAuctionSold)}>
<i className={'fa ' + getFontAwesomeClass(props.isAuctionCancelled, props.isAuctionSold)} data-test="header-font-awesome-icon" aria-hidden="true"></i>
<span data-test="auction-house-name">
{(props.auctionHouse ? props.auctionHouse.name : 'Missing Auction House Info')}
</span>
<i data-test="post-title-text">{getPostTitleText(props.isAuctionCancelled, props.isAuctionSold)}</i>
</h6>
</div>
<div className="col ">
<h6 className={'pull-right ' + getAuctionHeaderColorClass(props.isAuctionCancelled, props.isAuctionSold)}>
<i className={'fa ' + (props.isOpen ? 'fa-chevron-down' : 'fa-chevron-up')} data-test="chevron-icon" aria-hidden="true"></i>
</h6>
</div>
</div>
</div>
)
}
export default AuctionInfoSelectionHeader;
@@ -0,0 +1,232 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IAuctionAssignment } from "../../../../reducers/AuctionAssignmentReducer";
import { IRepoAccount } from "../../../../services/service-types/RepoAccountTypes";
import AuctionInfoSidebar from "./AuctionInfoSidebar";
describe("AuctionInfoSidebar component", () => {
let wrapper: ShallowWrapper;
let updatedRepoAccount: IRepoAccount;
let updatedAuctionAssignment: IAuctionAssignment;
beforeEach(() => {
wrapper = shallow(
<AuctionInfoSidebar
collateralImagePath={""}
conditionReportLink={""}
selectedAccount={dummyRepoAccount}
putRepoAccount={testUpdateRepoAccount}
auctionAssignment={dummyAuctionAssignment}
putAuctionAssignment={testUpdateAuctionAssignment}
holdStatuses={[]}
isAuctionLocked={false}
removedAuctionAssignment={() => {}}
/>);
});
const testUpdateRepoAccount = (repoAccount: IRepoAccount) => {
updatedRepoAccount = repoAccount;
};
const testUpdateAuctionAssignment = (auctionAssignment: IAuctionAssignment) => {
updatedAuctionAssignment = auctionAssignment;
};
const getCardView = () => {
return wrapper.find("CardView").dive();
};
it ("Redeems repo account without updating source", () => {
const redeemButton = getCardView().find(`[data-test='redeem-button']`);
redeemButton.simulate("click");
expect(updatedRepoAccount).toEqual({
...dummyRepoAccount,
status: "REDEEMED",
});
expect(updatedRepoAccount).not.toBe(dummyRepoAccount);
expect(updatedAuctionAssignment).toEqual({
...dummyAuctionAssignment,
redeemedDate: expect.any(moment)
});
expect(updatedAuctionAssignment).not.toBe(dummyAuctionAssignment);
});
it ("Disables redeem button when account has redeemed status", () => {
const redeemedAccount = {
...dummyRepoAccount,
status: "REDEEMED"
};
wrapper = shallow(
<AuctionInfoSidebar
collateralImagePath={""}
conditionReportLink={""}
selectedAccount={redeemedAccount}
putRepoAccount={testUpdateRepoAccount}
auctionAssignment={dummyAuctionAssignment}
putAuctionAssignment={testUpdateAuctionAssignment}
holdStatuses={[]}
isAuctionLocked={false}
removedAuctionAssignment={() => {}}
/>
);
const redeemButton = getCardView().find(`[data-test='redeem-button']`);
expect(redeemButton.props().disabled).toBeTruthy();
});
it ("Updates title sent date without updating source", () => {
const titleSentButton = getCardView().find(`[data-test='notify-title-sent-button']`);
titleSentButton.simulate("click");
expect(updatedAuctionAssignment).toEqual({
...dummyAuctionAssignment,
sentTitleDocsDate: expect.any(moment)
});
expect(updatedRepoAccount).not.toBe(dummyRepoAccount);
});
it ("Notify Title Sent & Redeemed Button Disabled When Auction is Locked", () => {
wrapper = wrapper.setProps({
isAuctionLocked: true
});
const titleSentButton = getCardView().find(`[data-test='notify-title-sent-button']`);
const redeemButton = getCardView().find(`[data-test='redeem-button']`);
expect(titleSentButton.props().disabled).toBe(true);
expect(redeemButton.props().disabled).toBe(true);
});
const dummyAuctionAssignment: IAuctionAssignment = {
id: 1,
repoId: 1,
isActive: true,
auctionAssignmentToCollaterals: [],
auctionId: "",
auctionCustomerNo: "",
cleaningLevel: 1,
collateralVIN: "",
floorPrice: 1,
floorPriceResponseCode: 1,
floorPriceResponseDescription: "Vehicle updated",
holdStatus: 1,
autoIMS: {
repoId: 1,
vin: "",
auctionAssignmentId: 1,
auctionCustomerNo: "",
announcedConditions: "condition 1; condition 2; condition 3;",
arbitrationReason: "",
asIs100KMiles: true,
auctionAge: 1,
auctionGuideBookPrice: 1,
buyerId: "",
buyerName: "",
chargeAmount: 1,
checkAmount: 1,
clientAge: 1,
collisionDamage: "",
daysRun: 1,
daysRunHighestBidRecordedbyAuction: "",
daystoDelivery: 1,
description: "",
isDrivable: true,
floorPriceAccepted: 1,
grossPercentFloor: 1,
highestBid: 1,
statedMileage: 1,
netPercentFloor: 1,
otherAdvanceFees: 1,
otherTotal: "",
otherAdvancedOther: "",
otherAdvancedStorage: "",
otherAdvancedTow: "",
otherMiscellaneous: "",
salePrice: 1,
titleNumber: "",
titleState: "",
titleStatus: "",
titleStatusfromAuction: "",
totalCharges: 1,
approvedVehicleGrade: 1,
isOperable: true,
status: "",
storageCost: 1,
storageRatePerDay: 1,
towCost: 1,
vehicleDecimalGradeCode: 1,
vehicleGrade: 1,
mmrNationalAdjustedPrice: 1,
mmrRegionalAdjustedPrice: 1,
mmr: "",
conditionReportLink: "",
collateralImagePath: "",
}
};
const dummyRepoAccount: IRepoAccount = {
repoId: 1,
accountNo: "",
status: "",
memberNo: 1,
loanNo: 1,
accountTypeCode: "",
branch: 1,
balance: 1,
dueToPayDate: moment(),
o4RDate: moment(),
dateFirstDelinquent: moment(),
delinquentAmount: 1,
daysDelinquent: 1,
paidSkipAmount: 1,
subStatus: "",
requiredPaymentAmount: 1,
currentDelinquentAmount: 1,
lstPmtAmt: 1,
origAmount: 1,
lastPaymentDate: moment(),
dateFirstIntellect: moment(),
primaryIndividualId: "",
bankruptcy: "",
manualRedemptionDate: moment(),
borrowers: [],
collaterals: [{
collateralId: 1,
repoId: 1,
vin: "",
make: "",
model: "",
year: 1,
repoCompanySelections: [],
retitledInLandmarkName: moment(),
auctionAssignmentId: 1,
auctionAssignmentDate: moment(),
}, {
collateralId: 1,
repoId: 1,
vin: "",
make: "",
model: "",
year: 1,
repoCompanySelections: [],
retitledInLandmarkName: moment(),
auctionAssignmentId: 2,
auctionAssignmentDate: moment(),
}, {
collateralId: 1,
repoId: 1,
vin: "",
make: "",
model: "",
year: 1,
repoCompanySelections: []
}],
repoStatusChanges: [],
};
});
@@ -0,0 +1,100 @@
import React from 'react';
import { IAuctionAssignment } from '../../../../reducers/AuctionAssignmentReducer';
import CardView from '../../../../components/card-view/CardView';
import moment, { Moment } from 'moment';
import { IHoldStatus } from '../../../../reducers/SettingsReducer';
import AuctionHoldInfo from './auction-hold-info/AuctionHoldInfo';
import { IRepoAccount } from '../../../../services/service-types/RepoAccountTypes';
export interface IAuctionInfoSidebarProps {
collateralImagePath: string;
conditionReportLink: string;
selectedAccount: IRepoAccount;
auctionAssignment: IAuctionAssignment;
holdStatuses: IHoldStatus[];
isAuctionLocked: boolean;
readonly putRepoAccount: (repoAccount: IRepoAccount) => any;
readonly putAuctionAssignment: (auctionAssignment: IAuctionAssignment) => any;
readonly removedAuctionAssignment: (auctionAssignment: IAuctionAssignment) => any;
}
const AuctionInfoSidebar: React.FC<IAuctionInfoSidebarProps> = (props: IAuctionInfoSidebarProps) => {
const updateRepoAccountStatus = (status: string, repoAccount: IRepoAccount) => {
const updated: IRepoAccount = {
...repoAccount,
status: status
}
props.putRepoAccount(updated);
}
const updateAuctionAssignmentRedeemedDate = (date: Moment, auctionAssignment: IAuctionAssignment) => {
const updated: IAuctionAssignment = {
...auctionAssignment,
redeemedDate: date
}
props.putAuctionAssignment(updated);
}
const redeemCollateral = () => {
updateRepoAccountStatus('REDEEMED', props.selectedAccount);
updateAuctionAssignmentRedeemedDate(moment(), props.auctionAssignment);
}
const updateAuctionAssignmentTitleSentDate = () => {
const updated: IAuctionAssignment = {
...props.auctionAssignment,
sentTitleDocsDate: moment()
}
props.putAuctionAssignment(updated);
}
return (
<div className="auto-ims-img-col">
<CardView rows={[(
<div>
<img className="auto-ims-image img-fluid rounded" src={props.collateralImagePath} alt="AutoIMS"/>
<a href={props.conditionReportLink} target="_blank" rel='noreferrer noopener'>AutoIMS Condition Report</a>
</div>
), (
<button
type="button"
className="btn btn-secondary auction-panel-btn"
onClick={ () => redeemCollateral()}
disabled={props.isAuctionLocked || props.selectedAccount.status === 'REDEEMED'}
data-test="redeem-button">
Redeem
</button>
), (
<button
type="button"
className="btn btn-secondary auction-panel-btn"
disabled={props.isAuctionLocked}
onClick={() => updateAuctionAssignmentTitleSentDate()}
data-test='notify-title-sent-button'
>
Notify Title Sent
</button>
), (
<AuctionHoldInfo
auctionAssignment={props.auctionAssignment}
saveAuctionAssignment={props.putAuctionAssignment}
repoAccount={props.selectedAccount}
saveRepoAccount={props.putRepoAccount}
holdStatuses={props.holdStatuses}
isAuctionLocked={props.isAuctionLocked}
/>
), (
<button
type="button"
className="btn btn-danger auction-panel-btn"
disabled={props.isAuctionLocked}
onClick={() => props.removedAuctionAssignment(props.auctionAssignment)}
data-test="remove-auction-button"
>
Remove Auction
</button>
)]} />
</div>
);
}
export default AuctionInfoSidebar;
@@ -0,0 +1,161 @@
import moment from "moment";
import React from "react";
import ReactDOM from "react-dom";
import { IAuctionAssignment } from "../../../../../reducers/AuctionAssignmentReducer";
import { IRepoAccount } from "../../../../../services/service-types/RepoAccountTypes";
import AuctionHoldInfo from "./AuctionHoldInfo";
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(
<AuctionHoldInfo
auctionAssignment={dummyAuctionAssignments[0]}
saveAuctionAssignment={() => {}}
repoAccount={dummyRepoAccount}
saveRepoAccount={() => {}}
holdStatuses={[]}
isAuctionLocked={false}
/>
, div);
ReactDOM.unmountComponentAtNode(div);
});
const dummyAuctionAssignments: IAuctionAssignment[] = [{
id: 1,
auctionId: "",
auctionCustomerNo: "",
cleaningLevel: 1,
collateralVIN: "",
floorPrice: 1,
floorPriceResponseCode: 1,
floorPriceResponseDescription: "Description",
holdStatus: 1,
autoIMS: {
repoId: 1,
vin: "",
auctionAssignmentId: 1,
auctionCustomerNo: "",
announcedConditions: "condition 1; condition 2; condition 3;",
arbitrationReason: "",
asIs100KMiles: true,
auctionAge: 1,
auctionGuideBookPrice: 1,
buyerId: "",
buyerName: "",
chargeAmount: 1,
checkAmount: 1,
clientAge: 1,
collisionDamage: "",
daysRun: 1,
daysRunHighestBidRecordedbyAuction: "",
daystoDelivery: 1,
description: "",
isDrivable: true,
floorPriceAccepted: 1,
grossPercentFloor: 1,
highestBid: 1,
statedMileage: 1,
netPercentFloor: 1,
otherAdvanceFees: 1,
otherTotal: "",
otherAdvancedOther: "",
otherAdvancedStorage: "",
otherAdvancedTow: "",
otherMiscellaneous: "",
salePrice: 1,
titleNumber: "",
titleState: "",
titleStatus: "",
titleStatusfromAuction: "",
totalCharges: 1,
approvedVehicleGrade: 1,
isOperable: true,
status: "",
storageCost: 1,
storageRatePerDay: 1,
towCost: 1,
vehicleDecimalGradeCode: 1,
vehicleGrade: 1,
mmrNationalAdjustedPrice: 1,
mmrRegionalAdjustedPrice: 1,
mmr: "",
conditionReportLink: "",
collateralImagePath: "",
}
}, {
id: 2,
auctionId: "",
auctionCustomerNo: "",
cleaningLevel: 1,
collateralVIN: "",
floorPrice: 1,
floorPriceResponseCode: 1,
holdStatus: 1,
} as IAuctionAssignment];
const dummyRepoAccount: IRepoAccount = {
repoId: 1,
accountNo: "",
status: "",
memberNo: 1,
loanNo: 1,
accountTypeCode: "",
branch: 1,
balance: 1,
dueToPayDate: moment(),
o4RDate: moment(),
dateFirstDelinquent: moment(),
delinquentAmount: 1,
daysDelinquent: 1,
paidSkipAmount: 1,
subStatus: "",
requiredPaymentAmount: 1,
currentDelinquentAmount: 1,
lstPmtAmt: 1,
origAmount: 1,
lastPaymentDate: moment(),
dateFirstIntellect: moment(),
primaryIndividualId: "",
bankruptcy: "",
manualRedemptionDate: moment(),
borrowers: [],
collaterals: [{
collateralId: 1,
repoId: 1,
vin: "",
make: "",
model: "",
year: 1,
isVoluntaryRepo: false,
repoCompanyAssignedDate: moment(),
dateRepoed: moment(),
repoCompanyId: 1,
retitledInLandmarkName: moment(),
auctionAssignmentId: 1,
auctionAssignmentDate: moment(),
}, {
collateralId: 1,
repoId: 1,
vin: "",
make: "",
model: "",
year: 1,
isVoluntaryRepo: false,
repoCompanyAssignedDate: moment(),
dateRepoed: moment(),
repoCompanyId: 1,
retitledInLandmarkName: moment(),
auctionAssignmentId: 2,
auctionAssignmentDate: moment(),
}, {
collateralId: 1,
repoId: 1,
vin: "",
make: "",
model: "",
year: 1,
isVoluntaryRepo: false,
repoCompanyId: 1,
}],
repoStatusChanges: [],
};
@@ -0,0 +1,164 @@
import React from "react";
import FormInput, { FormInputType, IFormInputSelectOption } from "../../../../../components/form-input/FormInput";
import { IAuctionAssignment } from "../../../../../reducers/AuctionAssignmentReducer";
import { IHoldStatus } from "../../../../../reducers/SettingsReducer";
import { IRepoAccount } from "../../../../../services/service-types/RepoAccountTypes";
import "./AuctionHoldInfo.css";
const cloneDeep = require("lodash.clonedeep");
interface IAuctionHoldInfoProps {
readonly auctionAssignment: IAuctionAssignment;
readonly saveAuctionAssignment: (x: IAuctionAssignment) => any;
readonly repoAccount: IRepoAccount;
readonly saveRepoAccount: (x: IRepoAccount) => any;
readonly holdStatuses: IHoldStatus[];
isAuctionLocked: boolean;
}
interface IAuctionHoldInfoState {
auctionAssignment: IAuctionAssignment;
repoAccount: IRepoAccount;
isOpen: boolean;
}
type IAuctionAssignmentUpdater = (assignment: IAuctionAssignment) => void;
class AuctionHoldInfo extends React.Component<IAuctionHoldInfoProps, IAuctionHoldInfoState> {
constructor(props: IAuctionHoldInfoProps) {
super(props);
this.state = {
auctionAssignment: cloneDeep(props.auctionAssignment),
repoAccount: cloneDeep(props.repoAccount),
isOpen: false,
};
}
toggleOpen = () => this.setState({ isOpen: !this.state.isOpen });
forceClosed = () => this.setState({ isOpen: false });
private applyChange(updater: IAuctionAssignmentUpdater) {
const copiedAssignment = cloneDeep(this.state.auctionAssignment);
updater(copiedAssignment);
this.setState({
auctionAssignment: copiedAssignment,
});
}
componentDidUpdate(prevProps: IAuctionHoldInfoProps, prevState: IAuctionHoldInfoState) {
if (prevState.auctionAssignment !== this.state.auctionAssignment) {
return;
}
if (prevProps.auctionAssignment !== this.props.auctionAssignment) {
this.setState({
auctionAssignment: cloneDeep(this.props.auctionAssignment)
});
}
}
private getHoldStatusOptions = (holdStatuses: IHoldStatus[]): IFormInputSelectOption[] => {
if (holdStatuses.length) {
return holdStatuses.map((holdStatus) => {
return {
value: holdStatus.code,
display: holdStatus.description
};
});
} else {
return [{
value: "",
display: "No Hold Statuses"
}];
}
}
private addAuctionAssignmentHold() {
this.props.saveAuctionAssignment(this.state.auctionAssignment);
}
private removeAuctionAssignmentHold() {
const auctionAssignment = cloneDeep(this.state.auctionAssignment);
auctionAssignment.holdStatus = 0;
auctionAssignment.holdUntil = undefined;
this.props.saveAuctionAssignment(auctionAssignment);
}
private maybeRenderRemoveHoldButton = () => {
if (this.props.auctionAssignment.holdStatus !== null) {
return (
<div className="form-group">
<button
className="form-control btn btn-danger auction-panel-btn"
onClick={() => { this.removeAuctionAssignmentHold(); }}
data-test="remove-hold-btn"
>
Remove Hold
</button>
</div>
);
}
}
render() {
const formClass = `${this.state.isOpen ? "" : " collapse"}`;
return (
<div>
<div>
<button
type="button"
className="btn btn-secondary auction-panel-btn"
data-toggle="collapse"
data-target="#collapseForm"
aria-controls="collapseForm"
aria-expanded="false"
disabled={this.props.isAuctionLocked}
onClick={this.toggleOpen}
>
Place On Hold
<span className="pull-right">
<i className={"fa " + (this.state.isOpen ? "fa-chevron-down" : "fa-chevron-up")} aria-hidden="true"/>
</span>
</button>
<span className="sr-only">Toggle Hold Form</span>
</div>
<div className={"auction-hold-info" + formClass}>
<div className="form-group">
<FormInput
label="Hold Status"
type={FormInputType.Selection}
value={this.state.auctionAssignment.holdStatus}
onChange={(value) => this.applyChange((a) => a.holdStatus = parseInt(value))}
options={this.getHoldStatusOptions(this.props.holdStatuses)}
disabled={!this.props.holdStatuses.length}
/>
</div>
<div className="form-group">
<FormInput
label="Hold Until"
type={FormInputType.Datetime}
value={this.state.auctionAssignment.holdUntil}
onChange={(value) => this.applyChange((a) => a.holdUntil = value)}
disabled={!this.props.holdStatuses.length}
/>
</div>
<div className="form-group">
<button
className="form-control btn btn-info auction-panel-btn"
onClick={() => { this.addAuctionAssignmentHold(); }}
disabled={!this.state.auctionAssignment.holdStatus || !this.state.auctionAssignment.holdUntil}
data-test="submit-hold-btn"
>
Submit
</button>
</div>
{this.maybeRenderRemoveHoldButton()}
</div>
</div>
);
}
}
export default AuctionHoldInfo;
@@ -0,0 +1,41 @@
import React from 'react';
import AuctionSoldBanner, { getSoldText } from './AuctionSoldBanner';
import { shallow } from 'enzyme';
import { IAutoIMS } from '../../../../reducers/AuctionAssignmentReducer';
import moment from 'moment';
describe('AuctionSoldBaner Component', () => {
it ('Sold Banner Does Not Display When AutoIMS is Undefined', () => {
const wrapper = shallow(<AuctionSoldBanner />);
const soldNotification = wrapper.find(`[data-test='auction-sold-banner-notification']`);
expect(soldNotification.length).toBe(0);
});
it ('Sold Banner Does Not Display When SalePrice is Undefined', () => {
const wrapper = shallow(<AuctionSoldBanner autoIMS={{ soldDate: moment() } as IAutoIMS} />);
const soldNotification = wrapper.find(`[data-test='auction-sold-banner-notification']`);
expect(soldNotification.length).toBe(0);
});
it ('Sold Banner Does Not Display When SoldDate is Undefined', () => {
const wrapper = shallow(<AuctionSoldBanner autoIMS={{ salePrice: 1916 } as IAutoIMS} />);
const soldNotification = wrapper.find(`[data-test='auction-sold-banner-notification']`);
expect(soldNotification.length).toBe(0);
});
it ('Sold Banner Displays Correct Text', () => {
const dummyAutoIMS = { salePrice: 1945, soldDate: moment('1945-09-02') } as IAutoIMS;
const wrapper = shallow(<AuctionSoldBanner autoIMS={dummyAutoIMS} />);
const soldNotification = wrapper.find(`[data-test='auction-sold-banner-notification']`);
expect(soldNotification.length).toBe(1);
const soldDateText = getSoldText(dummyAutoIMS);
expect(soldDateText).toBe('Sold on 09/02/1945 for $1,945.00. Funds Not Sent.');
dummyAutoIMS.fundsSent = moment();
const updatedSoldDateText = getSoldText(dummyAutoIMS);
expect(updatedSoldDateText).toBe('Sold on 09/02/1945 for $1,945.00. Funds Sent.');
});
});
@@ -0,0 +1,32 @@
import React from 'react';
import { IAutoIMS } from '../../../../reducers/AuctionAssignmentReducer';
import NotificationBar, { NotificationBarType } from '../../../../components/notification-bar/NotificationBar';
import DateFormatting from '../../../../shared/formatting/DateFormatting';
import MonetaryFormatting from '../../../../shared/formatting/MonetaryFormatting';
interface IAuctionSoldBannerProps {
autoIMS?: IAutoIMS;
}
export const getSoldText = (autoIMS: IAutoIMS): string => {
const fundStatus = autoIMS.fundsSent ? 'Funds Sent.' : 'Funds Not Sent.';
const formattedSaleDate = DateFormatting.FormatMMDDYYYY(autoIMS.soldDate);
const formattedSalePrice = MonetaryFormatting.FormatUSD(autoIMS.salePrice);
return `Sold on ${formattedSaleDate} for ${formattedSalePrice}. ${fundStatus}`;
}
const AuctionSoldBanner: React.FC<IAuctionSoldBannerProps> = (props) => {
if (props.autoIMS && props.autoIMS.soldDate && props.autoIMS.salePrice) {
return (
<NotificationBar
type={NotificationBarType.Success}
text={getSoldText(props.autoIMS)}
data-test="auction-sold-banner-notification"
/>
)
} else {
return null;
}
};
export default AuctionSoldBanner;
@@ -0,0 +1,62 @@
import React from 'react';
import { IAuctionAssignment } from "../../../../reducers/AuctionAssignmentReducer";
import { IRepoAccount, ICollateral } from "../../../../services/service-types/RepoAccountTypes";
import { IHoldStatus } from "../../../../reducers/SettingsReducer";
import CardView from "../../../../components/card-view/CardView";
import AuctionCollaterals from "../auction-collaterals/AuctionCollaterals";
import AutoIMSOverview from "../autoims-overview/AutoIMSOverview";
import AnnouncedConditions from "../announced-conditions/AnnouncedConditions";
import AuctionHouseInfo from "../auction-house-info/AuctionHouseInfo";
import Valuation from "../valuation/Valuation";
import AuctionInfoSidebar from "../auction-info-sidebar/AuctionInfoSidebar";
interface IAutoIMSDisplayProps {
auctionAssignment: IAuctionAssignment;
selectedAccount: IRepoAccount;
holdStatuses: IHoldStatus[];
collaterals: ICollateral[];
isAuctionLocked: boolean;
putAuctionAssignment: (auctionAssignment: IAuctionAssignment) => any;
putRepoAccount: (repoAccount: IRepoAccount) => any;
onAuctionAssignmentRemoved: (auctionAssignment: IAuctionAssignment) => any;
}
const AutoIMSDisplay: React.FC<IAutoIMSDisplayProps> = (props) => {
const auctionAssignment = props.auctionAssignment;
const auction = props.auctionAssignment.auction;
const autoIMS = props.auctionAssignment.autoIMS;
return (
<div className="autoims-general-info row">
<div className="col-md-8">
<CardView rows={[(
<AuctionCollaterals collaterals={props.collaterals} />), (
<AutoIMSOverview autoIMS={autoIMS} />), (
<AnnouncedConditions announcedConditions={autoIMS ? autoIMS.announcedConditions : undefined} />), (
<AuctionHouseInfo auctionHouse={auction} />), (
<Valuation
auctionAssignment={auctionAssignment}
selectedAccount={props.selectedAccount}
putAuctionAssignment={props.putAuctionAssignment}
isAuctionLocked={props.isAuctionLocked}
/>)
]}/>
</div>
<div className="col-md-4">
<AuctionInfoSidebar
collateralImagePath={autoIMS ? autoIMS.collateralImagePath : ''}
conditionReportLink={autoIMS ? autoIMS.conditionReportLink : ''}
selectedAccount={props.selectedAccount}
putRepoAccount={props.putRepoAccount}
auctionAssignment={auctionAssignment}
putAuctionAssignment={props.putAuctionAssignment}
removedAuctionAssignment={props.onAuctionAssignmentRemoved}
holdStatuses={props.holdStatuses}
isAuctionLocked={props.isAuctionLocked}
/>
</div>
</div>
);
};
export default AutoIMSDisplay;
@@ -0,0 +1,13 @@
import React from 'react';
import { shallow } from 'enzyme';
import AutoIMSOverview from './AutoIMSOverview';
describe('AutoIMSOverview Component', () => {
it ('Renders Error Notification When AutoIMS is Undefined', () => {
const wrapper = shallow(<AutoIMSOverview />);
const autoIMSNotification = wrapper.find(`[data-test='auto-ims-overview-data-missing']`);
expect(autoIMSNotification.length).toBe(1);
});
});

Some files were not shown because too many files have changed in this diff Show More