Adding
This commit is contained in:
+13
@@ -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;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.account-activity {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.account-activity-notable-dates {
|
||||
h5 {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
+101
@@ -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);
|
||||
});
|
||||
});
|
||||
+125
@@ -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);
|
||||
+44
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+44
@@ -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;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.notable-dates {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.notable-dates-edit {
|
||||
margin-left: 2.5em;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
margin-top: 1em;
|
||||
}
|
||||
+76
@@ -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');
|
||||
});
|
||||
});
|
||||
+121
@@ -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;
|
||||
+229
@@ -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;
|
||||
+65
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+83
@@ -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));
|
||||
+38
@@ -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;
|
||||
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+65
@@ -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);
|
||||
+26
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
*/}
|
||||
+333
@@ -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);
|
||||
+216
@@ -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);
|
||||
});
|
||||
});
|
||||
+326
@@ -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);
|
||||
+12
@@ -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;
|
||||
}
|
||||
+157
@@ -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);
|
||||
});
|
||||
});
|
||||
+73
@@ -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;
|
||||
+24
@@ -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>
|
||||
)
|
||||
}
|
||||
+79
@@ -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;
|
||||
+33
@@ -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;
|
||||
+72
@@ -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);
|
||||
});
|
||||
});
|
||||
+141
@@ -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));
|
||||
+371
@@ -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);
|
||||
});
|
||||
});
|
||||
+192
@@ -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;
|
||||
+75
@@ -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);
|
||||
});
|
||||
});
|
||||
+168
@@ -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;
|
||||
|
||||
+34
@@ -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");
|
||||
});
|
||||
});
|
||||
+35
@@ -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;
|
||||
|
||||
+34
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+46
@@ -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);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
.factoid-wdiget-view {
|
||||
.factoid-widget-parent {
|
||||
padding: 0.25em;
|
||||
}
|
||||
}
|
||||
+74
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+47
@@ -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;
|
||||
+52
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+38
@@ -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;
|
||||
+70
@@ -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);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
.admin-repo-companies-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.admin-repo-companies-table th {
|
||||
min-width: 7.5em;
|
||||
}
|
||||
+51
@@ -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;
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.admin-repo-company-creation-row {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
+61
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
+169
@@ -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"/>
|
||||
|
||||
Create
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AdminRepoCompanyCreation;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
.admin-repo-company-row-form button {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.disabled-repo-company-row p {
|
||||
opacity: 0.5;
|
||||
}
|
||||
+110
@@ -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!");
|
||||
});
|
||||
|
||||
});
|
||||
+280
@@ -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;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.admin-report-form {
|
||||
padding: 1em;
|
||||
}
|
||||
+23
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+67
@@ -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));
|
||||
+239
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
+103
@@ -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"> </i>
|
||||
: null}
|
||||
{ this.state.sortedColumn && this.state.sortedColumn.index === index && this.state.sortedColumn.isReversed ?
|
||||
<i className="fa fa-long-arrow-up" aria-hidden="true"> </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;
|
||||
+50
@@ -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");
|
||||
});
|
||||
|
||||
});
|
||||
+156
@@ -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"> </i>
|
||||
: null}
|
||||
{ this.state.sortedColumn && this.state.sortedColumn.index === index && this.state.sortedColumn.isReversed ?
|
||||
<i className="fa fa-long-arrow-up" aria-hidden="true"> </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);
|
||||
+110
@@ -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;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
.alert-bar-filter-tags .filter-tag {
|
||||
font-size: 0.85em;
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
+34
@@ -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}
|
||||
<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;
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
.auction-assignment-wizard .collections-history-table {
|
||||
max-height: 15em;
|
||||
overflow: auto;
|
||||
}
|
||||
+213
@@ -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);
|
||||
+83
@@ -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,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
});
|
||||
*/}
|
||||
+122
@@ -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;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.auction-collateral-location select, .auction-collateral-location input {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
+258
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+207
@@ -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);
|
||||
+115
@@ -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;
|
||||
+52
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
+61
@@ -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;
|
||||
+46
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
+40
@@ -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;
|
||||
+90
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
+47
@@ -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;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
.auction-info-assignment-header {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auction-info-assignment-header .fa {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
+94
@@ -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);
|
||||
});
|
||||
|
||||
})
|
||||
+77
@@ -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;
|
||||
+232
@@ -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: [],
|
||||
};
|
||||
|
||||
});
|
||||
+100
@@ -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;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.auction-hold-info {
|
||||
margin-top: .5em;
|
||||
}
|
||||
+161
@@ -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: [],
|
||||
};
|
||||
+164
@@ -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;
|
||||
+41
@@ -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.');
|
||||
});
|
||||
|
||||
});
|
||||
+32
@@ -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;
|
||||
+62
@@ -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;
|
||||
+13
@@ -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
Reference in New Issue
Block a user