This commit is contained in:
Zeeshaun
2026-03-17 11:09:46 -05:00
parent bd1a9744ad
commit 6e7edcfe1b
332 changed files with 64174 additions and 0 deletions
@@ -0,0 +1,33 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,18 @@
import React from 'react';
import App from './App';
import { ShallowWrapper, shallow } from 'enzyme';
// - 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 ('App component', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(<App />);
});
it ('renders a navigation bar', () => {
const component = wrapper.find(`[data-test='navigationBar']`);
expect(component.length).toBe(1);
});
});
@@ -0,0 +1,41 @@
import { ConnectedRouter } from "connected-react-router";
import React from "react";
import { HashRouter, Route, Switch } from "react-router-dom";
import "./App.css";
import Footer from "./components/footer/Footer";
import AccountPage from "./containers/account-page/AccountPage";
import AdminAuctionHouses from "./containers/admin-auction-houses/AdminAuctionHouses";
import AdminDashBoard from "./containers/admin-dashboard/AdminDashboard";
import AdminRepoCompanies from "./containers/admin-repo-companies/AdminRepoCompanies";
import AdminReporting from "./containers/admin-reporting/AdminReporting";
import FloorCalculation from "./containers/floor-calculation/FloorCalculation";
import NavigationBar from "./containers/navigation-bar/NavigationBar";
import StartPage from "./containers/start-page/StartPage";
import VehicleFloorPricingParameters from "./containers/vehicle-floor-pricing-parameters/VehicleFloorPricingParameters";
import AdminAccountLookup from "./containers/admin-account-lookup/AdminAccountLookup";
import Routes from "./shared/Routes";
import { AppRouteHistory } from "./store/Store";
const App: React.FC = () => {
return (
<ConnectedRouter history={AppRouteHistory}>
<HashRouter>
<NavigationBar data-test="navigationBar"/>
<Switch>
<Route exact={true} path={Routes.Root()} render={(routerProps) => <StartPage {...routerProps} data-test="startPage"/>} />
<Route exact={true} path={Routes.Account_Decl()} render={(routerProps) => <AccountPage {...routerProps} data-test="accountPage"/>} />
<Route exact={true} path={Routes.Admin()} render={(routerProps) => <AdminDashBoard {...routerProps} data-test="adminDashBoardLnk"/>} />
<Route exact={true} path={Routes.Reporting()} render={(routerProps) => <AdminReporting {...routerProps} data-test="adminReportingLnk"/>} />
<Route exact={true} path={Routes.FloorPricing()} render={(routerProps) => <VehicleFloorPricingParameters {...routerProps} data-test="floorPricingParameters"/>} />
<Route exact={true} path={Routes.AdminAuctionHouses()} render={(routerProps) => <AdminAuctionHouses {...routerProps} />} />
<Route exact={true} path={Routes.FloorCalculation_Decl()} render={(routerProps) => <FloorCalculation {...routerProps}/>} />
<Route exact={true} path={Routes.AdminRepoCompanies()} render={(routerProps) => <AdminRepoCompanies {...routerProps} />} />
<Route exact={true} path={Routes.AdminAccountLookup()} render={(routerProps) => <AdminAccountLookup {...routerProps} />} />
</Switch>
<Footer data-test="footer"/>
</HashRouter>
</ConnectedRouter>
);
};
export default App;
@@ -0,0 +1,45 @@
import moxios from 'moxios';
import {applyMiddleware, createStore} from 'redux';
import rootReducer from '../store/Store';
import thunk from 'redux-thunk';
import {getAllAlerts} from './AlertActions';
const testStore = (initialState: any) => {
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
return createStoreWithMiddleware(rootReducer, initialState);
};
describe('getAllAlerts action', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
test('Store updates correctly', () => {
const expectedState = {
"alerts": [
{stuff: 1}
]
};
const store = testStore(expectedState);
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: expectedState
});
});
const beginningState = store.getState().alertState;
store.dispatch(getAllAlerts()).then(() => {
const newState = store.getState().alertState;
expect(newState).toEqual(expectedState);
expect(newState).not.toEqual(beginningState);
});
});
});
@@ -0,0 +1,141 @@
// Import redux types
import { ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import axios from 'axios';
// Import Alert Typing
import { IAlert, IAlertState } from '../reducers/AlertReducer';
import Configuration from '../shared/configuration/Configuration';
import ServerLogging from '../shared/logging/ServerLogging';
import UrlFormatting from '../shared/formatting/UrlFormatting';
import moment from 'moment';
import DateConversions from '../shared/conversions/DateConversions';
import {getUser} from '../adalConfig';
const cloneDeep = require('lodash.clonedeep');
// Create Action Constants
export enum AlertActionTypes {
GET_ALL = 'GET_ALL',
GET_ALL_ACTIONS = 'GET_ALL_ACTIONS',
MARK_ALERT_READ = 'MARK_ALERT_READ',
}
// Interface for Get All Action Type
export interface IAlertGetAllAction {
type: AlertActionTypes.GET_ALL;
alerts: IAlert[];
}
export interface IAlertGetAllActionsAction {
type: AlertActionTypes.GET_ALL_ACTIONS;
actions: IAlert[];
}
export interface IAlertMarkReadAction {
type: AlertActionTypes.MARK_ALERT_READ,
alert: IAlert;
}
/*
Combine the action types with a union (we assume there are more)
example: export type AlertActions = IGetAllAction | IGetOneAction ...
*/
export type AlertActions = IAlertGetAllAction | IAlertGetAllActionsAction | IAlertMarkReadAction;
const convertServerDataToAlerts = (alerts: IAlert[]): IAlert[] => {
return alerts.map((alert) => convertServerDataToAlert(alert));
}
const convertServerDataToAlert = (alert: IAlert): IAlert => {
alert.dateMarkedRead = DateConversions.utcToMoment(alert.dateMarkedRead);
alert.dateAdded = DateConversions.utcToMoment(alert.dateAdded)!;
return alert;
};
const createAPIReadyAlert = (alert: IAlert) => {
const clonedAlert = cloneDeep(alert);
clonedAlert.dateAdded = DateConversions.momentToUTC(alert.dateAdded);
clonedAlert.dateMarkedRead = DateConversions.momentToUTC(alert.dateMarkedRead);
return clonedAlert;
}
/* Get All Action
<Promise<Return Type>, State Interface, Type of Param, Type of Action> */
export const getAllAlerts: ActionCreator<
ThunkAction<Promise<any>, IAlertState, null, IAlertGetAllAction>
> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.alerts_unread);
return async (dispatch: Dispatch) => {
try {
const response = await axios.get(url);
const convertedAlerts = convertServerDataToAlerts(response.data);
dispatch({
alerts: convertedAlerts,
type: AlertActionTypes.GET_ALL,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err,{
action: "Get All Alerts",
user: getUser().userName,
url: url
});
}
};
};
// @TODO, the action should just take the type of alert to load?
export const getAllActions: ActionCreator<
ThunkAction<Promise<any>, IAlertState, null, IAlertGetAllActionsAction>
> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.alerts_action_needed);
return async (dispatch: Dispatch) => {
try {
const response = await axios.get(url);
dispatch({
actions: response.data,
type: AlertActionTypes.GET_ALL_ACTIONS,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err,{
action: "Get All Actions",
user: getUser().userName,
url: url
});
}
};
};
export const markAlertRead: ActionCreator<
ThunkAction<Promise<any>, IAlertState, null, IAlertMarkReadAction>
> = (alert: IAlert) => {
const alertPostURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.alerts_put),
{ key: 'alertId', value: alert.id },
);
return async (dispatch: Dispatch) => {
try {
const clonedAlert = cloneDeep(alert) as IAlert;
clonedAlert.dateMarkedRead = moment();
const revertedAlert = createAPIReadyAlert(clonedAlert);
const response = await axios.put(
alertPostURL,
revertedAlert,
);
dispatch({
type: AlertActionTypes.MARK_ALERT_READ,
alert: response.data,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err,{
action: "Mark Alert Read",
user: getUser().userName,
url: alertPostURL,
arguments: alert
});
}
}
};
@@ -0,0 +1,344 @@
import axios from "axios";
import { LocationChangeAction } from "connected-react-router";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IAuctionAssignment, IAuctionAssignmentPostRequest, IAuctionAssignmentState, IAutoIMS, IFloorPriceCalculationResultSet, IFloorPriceValuesUsed } from "../reducers/AuctionAssignmentReducer";
import RepoAccountService from "../services/RepoAccountService";
import Configuration from "../shared/configuration/Configuration";
import DateConversions from "../shared/conversions/DateConversions";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from '../adalConfig'
const cloneDeep = require("lodash.clonedeep");
export enum AuctionAssignmentActionTypes {
GET_FLOOR_PRICE = "AUCTION_ASSIGNMENT_GET_FLOOR_PRICE",
INSERT = "AUCTION_ASSIGNMENT_INSERT",
INSERT_FROM_POST = "AUCTION_ASSIGNMENT_INTSERT_FROM_POST",
INSERT_MULTIPLE = "AUCTION_ASSIGNMENT_INSERT_MULTIPLE",
SET_SELECTED_AUCTION_ASSIGNMENT = "AUCTION_ASSIGNMENT_SET_SELECTED"
}
export interface IAuctionAssignmentGetFloorPrice {
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE;
floorPriceCalculationResultSet: IAsyncStateContainer<IFloorPriceCalculationResultSet>;
}
export interface IAuctionAssignmentInsert {
type: AuctionAssignmentActionTypes.INSERT;
auctionAssignment: IAuctionAssignment;
}
export interface IAuctionAssignmentInsertFromPost {
type: AuctionAssignmentActionTypes.INSERT_FROM_POST;
auctionAssignmentResponse: IAuctionAssignmentPostRequest;
}
export interface IAuctionAssignmentInsertMultiple {
type: AuctionAssignmentActionTypes.INSERT_MULTIPLE;
auctionAssignments: IAuctionAssignment[];
}
export interface IAuctionAssignmentSetSelected {
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT;
auctionAssignmentId: IAsyncStateContainer<number>;
}
export type AuctionAssignmentActions =
IAuctionAssignmentGetFloorPrice |
IAuctionAssignmentInsert |
IAuctionAssignmentInsertMultiple |
IAuctionAssignmentInsertFromPost |
IAuctionAssignmentSetSelected |
LocationChangeAction;
const convertServerDataToAuctionAssignment = (auctionAssignment: IAuctionAssignment): void => {
auctionAssignment.dateSubmitted = DateConversions.utcToMoment(auctionAssignment.dateSubmitted);
auctionAssignment.holdUntil = DateConversions.utcToMoment(auctionAssignment.holdUntil);
auctionAssignment.redeemedDate = DateConversions.utcToMoment(auctionAssignment.redeemedDate);
auctionAssignment.sentTitleDocsDate = DateConversions.utcToMoment(auctionAssignment.sentTitleDocsDate);
if (auctionAssignment.autoIMS) {
const autoIMS: IAutoIMS = auctionAssignment.autoIMS;
autoIMS.assignmentAccepted = DateConversions.utcToMoment(autoIMS.assignmentAccepted);
autoIMS.dateReceivedFromAutoIMS = DateConversions.utcToMoment(autoIMS.dateReceivedFromAutoIMS);
autoIMS.scheduledSaleDate = DateConversions.utcToMoment(autoIMS.scheduledSaleDate);
autoIMS.soldDate = DateConversions.utcToMoment(autoIMS.soldDate);
autoIMS.clientCancelled = DateConversions.utcToMoment(autoIMS.clientCancelled);
autoIMS.inspectedDate = DateConversions.utcToMoment(autoIMS.inspectedDate);
autoIMS.dateofLoss = DateConversions.utcToMoment(autoIMS.dateofLoss);
autoIMS.datePickedUp = DateConversions.utcToMoment(autoIMS.datePickedUp);
autoIMS.dateEnteredAutoIMS = DateConversions.utcToMoment(autoIMS.dateEnteredAutoIMS);
autoIMS.fundsSent = DateConversions.utcToMoment(autoIMS.fundsSent);
autoIMS.titleReceived = DateConversions.utcToMoment(autoIMS.titleReceived);
autoIMS.titleReturned = DateConversions.utcToMoment(autoIMS.titleReturned);
autoIMS.shipmentDate = DateConversions.utcToMoment(autoIMS.shipmentDate);
auctionAssignment.autoIMS = autoIMS;
}
if (auctionAssignment.floorPriceValuesUsed) {
convertServerDataToFloorPriceValuesUsed(auctionAssignment.floorPriceValuesUsed);
}
};
const convertServerDataToFloorPriceValuesUsed = (floorPriceValuesUsed: IFloorPriceValuesUsed): void => {
floorPriceValuesUsed.collateralValuation.lookupDate = DateConversions.utcToMoment(floorPriceValuesUsed.collateralValuation.lookupDate);
};
const createAPIReadyAuctionAssignment = (auctionAssignment: IAuctionAssignment): any => {
const result = cloneDeep(auctionAssignment);
result.dateSubmitted = DateConversions.momentToUTC(auctionAssignment.dateSubmitted);
result.holdUntil = DateConversions.momentToUTC(auctionAssignment.holdUntil);
result.redeemedDate = DateConversions.momentToUTC(auctionAssignment.redeemedDate);
result.titleSentNotificationDate = DateConversions.momentToUTC(auctionAssignment.sentTitleDocsDate);
if (auctionAssignment.autoIMS) {
const autoIMS: any = cloneDeep(auctionAssignment.autoIMS);
autoIMS.assignmentAccepted = DateConversions.momentToUTC(autoIMS.assignmentAccepted);
autoIMS.dateReceivedFromAutoIMS = DateConversions.momentToUTC(autoIMS.dateReceivedFromAutoIMS);
autoIMS.scheduledSaleDate = DateConversions.momentToUTC(autoIMS.scheduledSaleDate);
autoIMS.soldDate = DateConversions.momentToUTC(autoIMS.soldDate);
autoIMS.clientCancelled = DateConversions.momentToUTC(autoIMS.clientCancelled);
autoIMS.inspectedDate = DateConversions.momentToUTC(autoIMS.inspectedDate);
autoIMS.dateofLoss = DateConversions.momentToUTC(autoIMS.dateofLoss);
autoIMS.datePickedUp = DateConversions.momentToUTC(autoIMS.datePickedUp);
autoIMS.dateEnteredAutoIMS = DateConversions.momentToUTC(autoIMS.dateEnteredAutoIMS);
autoIMS.fundsSent = DateConversions.momentToUTC(autoIMS.fundsSent);
autoIMS.titleReceived = DateConversions.momentToUTC(autoIMS.titleReceived);
autoIMS.titleReturned = DateConversions.momentToUTC(autoIMS.titleReturned);
autoIMS.shipmentDate = DateConversions.momentToUTC(autoIMS.shipmentDate);
result.autoIMS = autoIMS;
}
if (auctionAssignment.floorPriceValuesUsed) {
const valuesUsed: any = cloneDeep(auctionAssignment.floorPriceValuesUsed);
const collateralValuation: any = cloneDeep(auctionAssignment.floorPriceValuesUsed.collateralValuation);
collateralValuation.LookUpDate = DateConversions.momentToUTC(collateralValuation.LookUpDate);
valuesUsed.collateralValuation = collateralValuation;
result.valuesUsed = valuesUsed;
}
return result;
};
const createAPIReadyAuctionAssignmentPostRequest = (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest) => {
return {
auctionAssignment: createAPIReadyAuctionAssignment(auctionAssignmentPostRequest.auctionAssignment),
collaterals: auctionAssignmentPostRequest.collaterals,
vehicleLocation: auctionAssignmentPostRequest.vehicleLocation,
primaryCollateralId: auctionAssignmentPostRequest.primaryCollateralId,
};
};
const convertServerDataToAuctionAssignmentPostRequest = (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest): IAuctionAssignmentPostRequest => {
auctionAssignmentPostRequest.collaterals.map((collateral) => RepoAccountService.ConvertResponseToCollateral(collateral));
convertServerDataToAuctionAssignment(auctionAssignmentPostRequest.auctionAssignment);
return {
auctionAssignment: auctionAssignmentPostRequest.auctionAssignment,
collaterals: auctionAssignmentPostRequest.collaterals,
vehicleLocation: auctionAssignmentPostRequest.vehicleLocation,
primaryCollateralId: auctionAssignmentPostRequest.primaryCollateralId,
};
};
export const getAuctionAssignmentsByRepoId: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsertMultiple>> = (repoId: number) => {
const auctionAssignmentURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_by_repo_id),
{ key: "repoId", value: repoId}
);
return async (dispatch: Dispatch<IAuctionAssignmentInsertMultiple>) => {
try {
const response = await axios.get(auctionAssignmentURL);
for (const auctionAssignment of response.data) {
convertServerDataToAuctionAssignment(auctionAssignment);
}
dispatch({
type: AuctionAssignmentActionTypes.INSERT_MULTIPLE,
auctionAssignments: response.data,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Auction Assignments by Repo ID",
user: getUser().userName,
url: auctionAssignmentURL,
arguments: repoId
});
}
};
};
export const fetchAuctionAssignment: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsert | IAuctionAssignmentSetSelected>> = (id: number) => {
const auctionAssignmentURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_single),
{ key: "auctionAssignmentId", value: id}
);
return async (dispatch: Dispatch<IAuctionAssignmentInsert | IAuctionAssignmentSetSelected>) => {
dispatch({
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT,
auctionAssignmentId: {
state: AsyncState.Loading
}
});
try {
const response = await axios.get(auctionAssignmentURL);
convertServerDataToAuctionAssignment(response.data);
dispatch({
type: AuctionAssignmentActionTypes.INSERT,
auctionAssignment: response.data,
});
dispatch({
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT,
auctionAssignmentId: {
state: AsyncState.Success,
data: id
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Fetch Auction Assignment",
user: getUser().userName,
url: auctionAssignmentURL,
arguments: id
});
dispatch({
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT,
auctionAssignmentId: {
state: AsyncState.Failure
}
});
}
};
};
export const getAuctionAssignmentFloorCalculations: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentGetFloorPrice>> = (auctionAssignmentId: number) => {
const auctionAssignmentURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_floor_price),
{ key: "auctionAssignmentId", value: auctionAssignmentId}
);
return async (dispatch: Dispatch<IAuctionAssignmentGetFloorPrice>) => {
dispatch({
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE,
floorPriceCalculationResultSet: {
state: AsyncState.Loading
}
});
try {
const response = await axios.get(auctionAssignmentURL);
const floorPriceCalculationResultSet = response.data as IFloorPriceCalculationResultSet;
convertServerDataToFloorPriceValuesUsed(floorPriceCalculationResultSet.floorPriceValuesUsed);
dispatch({
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE,
floorPriceCalculationResultSet: {
state: AsyncState.Success,
data: floorPriceCalculationResultSet
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Auction Assignment Floor Calculations",
user: getUser().userName,
url: auctionAssignmentURL,
arguments: auctionAssignmentId
});
dispatch({
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE,
floorPriceCalculationResultSet: {
state: AsyncState.Failure
}
});
}
};
};
export const postNewAuctionAssignment: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsertFromPost>> = (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment);
return async (dispatch: Dispatch<IAuctionAssignmentInsertFromPost>) => {
try {
const formattedRequest = createAPIReadyAuctionAssignmentPostRequest(auctionAssignmentPostRequest);
const response = await axios.post(url, formattedRequest);
const convertedData = convertServerDataToAuctionAssignmentPostRequest(response.data);
dispatch({
type: AuctionAssignmentActionTypes.INSERT_FROM_POST,
auctionAssignmentResponse: convertedData,
});
toast.success("Auction Assignment Saved.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post New Auction Assignment",
user: getUser().userName,
url: url,
arguments: auctionAssignmentPostRequest
});
}
};
};
export const putAuctionAssignment: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsert>> = (assignment: IAuctionAssignment) => {
const url = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_single),
{ key: "auctionAssignmentId", value: assignment.id}
);
return async (dispatch: Dispatch) => {
try {
const revertedAccount = createAPIReadyAuctionAssignment(assignment);
const response = await axios.put(url, revertedAccount);
const updatedAssignment = response.data;
convertServerDataToAuctionAssignment(updatedAssignment);
dispatch({
type: AuctionAssignmentActionTypes.INSERT,
auctionAssignment: updatedAssignment
});
toast.success("Auction Assignment Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Auction Assignment",
user: getUser().userName,
url: url,
arguments: assignment
});
}
};
};
export const removeAuction: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsert>> = (assignmentId: number) => {
const url = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_single),
{ key: "auctionAssignmentId", value: assignmentId}
);
return async (dispatch: Dispatch) => {
try {
const response = await axios.delete(url);
const updatedAssignment = response.data;
convertServerDataToAuctionAssignment(updatedAssignment);
dispatch({
type: AuctionAssignmentActionTypes.INSERT,
auctionAssignment: updatedAssignment
});
toast.success("Auction Assignment Removed.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Remove Auction",
user: getUser().userName,
url: url,
arguments: assignmentId
});
}
};
};
@@ -0,0 +1,160 @@
import Axios from "axios";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import {getUser} from "../adalConfig";
import { IAuctionHouse, IAuctionHouseState } from "../reducers/AuctionHouseReducer";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
export enum AuctionHouseActionTypes {
GET_AUCTION_HOUSES = "GET_AUCTION_HOUSES",
POST_AUCTION_HOUSE = "POST_AUCTION_HOUSE",
PUT_AUCTION_HOUSE = "PUT_AUCTION_HOUSE"
}
export interface IAuctionHouseGetAllAction {
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES;
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
}
export interface IAuctionHousePostAction {
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE;
auctionHouse: IAsyncStateContainer<IAuctionHouse>;
}
export interface IAuctionHousePutAction {
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE;
auctionHouse: IAsyncStateContainer<IAuctionHouse>;
}
export type AuctionHouseActions = IAuctionHouseGetAllAction | IAuctionHousePostAction | IAuctionHousePutAction;
export const getAllAuctionHouses: ActionCreator<ThunkAction<Promise<any>, IAuctionHouseState, null, IAuctionHouseGetAllAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_house);
return async (dispatch: Dispatch<IAuctionHouseGetAllAction>) =>{
dispatch({
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES,
auctionHouses: {
state: AsyncState.Loading,
},
});
try {
const response = await Axios.get(url);
const auctionHouses = (response.data as IAuctionHouse[]).sort((a, b) =>
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 0
);
dispatch({
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES,
auctionHouses: {
data: auctionHouses,
state: AsyncState.Success,
},
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Auction Houses",
user: getUser().userName,
url: url,
});
dispatch({
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES,
auctionHouses: {
state: AsyncState.Failure,
},
});
}
};
};
export const postAuctionHouse: ActionCreator<ThunkAction<Promise<any>, IAuctionHouseState, null, IAuctionHousePostAction>> = (newAuctionHouse: IAuctionHouse) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_house);
return async (dispatch: Dispatch<IAuctionHousePostAction>) =>{
try {
dispatch({
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Loading,
}
});
const response = await Axios.post(url, newAuctionHouse);
dispatch({
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Success,
data: response.data
}
});
toast.success("Auction House Added.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post New Auction House",
user: getUser().userName,
url: url,
arguments: newAuctionHouse
});
dispatch({
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Failure,
}
});
}
};
};
export const putAuctionHouse: ActionCreator<ThunkAction<Promise<any>, IAuctionHouseState, null, IAuctionHousePutAction>> = (updatedAuctionHouse: IAuctionHouse) => {
const url = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_house_by_auction_id),
{key:"auctionId", value: updatedAuctionHouse.auctionID},
);
return async (dispatch: Dispatch<IAuctionHousePutAction>) =>{
try {
dispatch({
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Loading,
}
});
const response = await Axios.put(url, updatedAuctionHouse);
dispatch({
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Success,
data: response.data
}
});
toast.success("Auction House Added.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Updated Auction House",
user: getUser().userName,
url: url,
arguments: updatedAuctionHouse
});
dispatch({
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Failure,
}
});
}
};
};
@@ -0,0 +1,191 @@
import Axios from "axios";
import {ActionCreator, Dispatch} from "redux";
import {ThunkAction} from "redux-thunk";
import {getUser} from "../adalConfig";
import {ICollectionsAccount, ICollectionsAccountState} from "../reducers/CollectionsAccountReducer";
import CollectionsAccountService from "../services/CollectionsAccountService";
import {IRepoAccount} from "../services/service-types/RepoAccountTypes";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {IAppState} from "../store/Store";
export enum CollectionsAccountActionTypes {
GET_COLLECTIONS_ACCOUNT = "GET_COLLECTIONS_ACCOUNT",
SET_SEARCH_PHRASE = "SET_SEARCH_PHRASE",
FIND_ASSOCIATED_REPOS = "FIND_ASSOCIATED_REPOS",
PUT_ACCOUNT_ON_QUEUE = "PUT_ACCOUNT_ON_QUEUE"
}
export interface ICollectionsAccountGetAction {
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT;
collectionsAccount: IAsyncStateContainer<ICollectionsAccount[]>;
}
export interface ICollectionsSetSearchPhraseAction {
type: CollectionsAccountActionTypes.SET_SEARCH_PHRASE;
searchPhrase: string;
}
export interface ICollectionsfindAssociatedRepoAccountAction {
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS;
associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]>;
}
export interface ICollectionsAccountPutAction {
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE;
didAddToQueue: IAsyncStateContainer<boolean>;
}
export type CollectionsAccountActions =
ICollectionsAccountGetAction
| ICollectionsSetSearchPhraseAction
| ICollectionsfindAssociatedRepoAccountAction
| ICollectionsAccountPutAction;
export const getCollectionsAccount: ActionCreator<ThunkAction<Promise<any>, ICollectionsAccountState, null, ICollectionsAccountGetAction>> = (accountNo: string) => {
const url = UrlFormatting.ReplaceURLParams(Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.collections_account), {
key: "accountNo",
value: Configuration.Get.collectionsAccountNoPrefix + accountNo
});
return async (dispatch: Dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT,
collectionsAccount: {
state: AsyncState.Loading
}
});
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
state: AsyncState.Inactive,
data: false
}
});
try {
const response = await Axios.get(url);
dispatch({
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT,
collectionsAccount: {
data: CollectionsAccountService.mapResponseToCollectionsAccount(response.data),
state: AsyncState.Success
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Collections Account",
user: getUser().userName,
url: url,
arguments: [accountNo]
});
dispatch({
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT,
collectionsAccount: {
state: AsyncState.Failure
}
});
}
};
};
export const setSearchPhraseCollectionsAccounts: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, ICollectionsSetSearchPhraseAction | ICollectionsAccountGetAction>> = (searchPhrase: string) => {
return async (dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: searchPhrase
});
};
};
export const findAssociatedRepoAccount: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, ICollectionsfindAssociatedRepoAccountAction>> = (collectionsAccount: ICollectionsAccount) => {
const url = UrlFormatting.ReplaceURLParams(Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single_by_account_no), {
key: "accountNo",
value: collectionsAccount.accountIdentifier
});
return async (dispatch: Dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
state: AsyncState.Loading
}
});
try {
const response = await Axios.get(url);
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
data: response.data,
state: AsyncState.Success
}
});
} catch (err) {
if( err && err.response && err.response.status !== 404 ) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Find Associated Repos",
user: getUser().userName,
url: url,
arguments: [collectionsAccount.accountIdentifier]
});
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
state: AsyncState.Failure
}
});
} else {
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
data: [],
state: AsyncState.Success
}
});
}
}
};
};
export const putAccountOnRepoQueue: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, ICollectionsAccountPutAction>> = (collectionsAccount: ICollectionsAccount) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.put_to_repo_queue);
const queueRequest = {
accountNo: collectionsAccount.accountIdentifier
};
return async (dispatch: Dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
state: AsyncState.Loading,
data: true
}
});
try {
await Axios.post(url, queueRequest);
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
state: AsyncState.Success,
data: true
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Account On Repo Queue",
user: getUser().userName,
url: url,
arguments: [collectionsAccount]
});
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
AsyncState: AsyncState.Failure,
data: true
}
});
}
};
};
@@ -0,0 +1,78 @@
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IFactoidState } from "../reducers/FactoidReducer";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import Axios from "axios";
import Configuration from "../shared/configuration/Configuration";
import ServerLogging from "../shared/logging/ServerLogging";
import { IFactoid, IFactoidResponse } from "../services/service-types/FactoidTypes";
import { RepoAccountActionTypes, IRepoAccountInsertAccounts } from "./RepoAccountActions";
import FactoidService from "../services/FactoidService";
import {getUser} from "../adalConfig";
export enum FactoidActionTypes {
SET_SELECTED_FACTOID_INDEX = 'FACTOID_SET_INDEX',
GET_ALL_FACTOIDS = 'FACTOIDs_GET_ALL'
}
export interface IFactoidSetSelectedIndexAction {
type: FactoidActionTypes.SET_SELECTED_FACTOID_INDEX;
index: number;
}
export interface IFactoidGetAllActions {
type: FactoidActionTypes.GET_ALL_FACTOIDS;
factoids: IAsyncStateContainer<IFactoid[]>;
}
export type FactoidActions =
IFactoidSetSelectedIndexAction &
IFactoidGetAllActions;
export const setSelectedFactoidIndex = (index: number): IFactoidSetSelectedIndexAction => {
return {
type: FactoidActionTypes.SET_SELECTED_FACTOID_INDEX,
index: index
}
};
export const getAllFactoids: ActionCreator<ThunkAction<Promise<any>, IFactoidState, null, IFactoidGetAllActions>> = (factoidState: IFactoidState) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.factoids_get_all);
return async(dispatch: Dispatch<IFactoidGetAllActions | IRepoAccountInsertAccounts>) => {
dispatch({
type: FactoidActionTypes.GET_ALL_FACTOIDS,
factoids: {
state: AsyncState.Loading
}
});
try {
const response = await Axios.get(url);
const factoidResponses = response.data as IFactoidResponse[];
dispatch({
type: RepoAccountActionTypes.INSERT_ACCOUNTS,
repoAccounts: FactoidService.ExtractRepoAccountsFromFactoidResponses(factoidResponses)
});
dispatch({
type: FactoidActionTypes.GET_ALL_FACTOIDS,
factoids: {
state: AsyncState.Success,
data: FactoidService.CreateFactoidsFromResponse(factoidResponses)
}
});
} catch(err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Factoids",
user: getUser().userName,
url: url,
arguments: factoidState
});
dispatch({
type: FactoidActionTypes.GET_ALL_FACTOIDS,
factoids: {
state: AsyncState.Failure
}
});
}
};
};
@@ -0,0 +1,45 @@
import moxios from 'moxios';
import {applyMiddleware, createStore} from 'redux';
import rootReducer from '../store/Store';
import thunk from 'redux-thunk';
import {markCollateralRetitled} from './RepoAccountActions'
const testStore = (initialState: any) => {
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
return createStoreWithMiddleware(rootReducer, initialState);
}
describe('markCollateralRetitled action', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
test('Store updates correctly', () => {
const expectedState = {
"repoAccounts": [
{stuff: 1}
]
};
const store = testStore(expectedState);
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: expectedState
});
});
const beginningState = store.getState().repoAccountState;
store.dispatch(markCollateralRetitled()).then(() => {
const newState = store.getState().repoAccountState;
expect(newState).toEqual(expectedState);
expect(newState).not.toEqual(beginningState);
});
});
});
@@ -0,0 +1,546 @@
import axios from "axios";
import { LocationChangeAction } from "connected-react-router";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import RepoAccountService from "../services/RepoAccountService";
import { ICollateral, IRefundStatus, IRepoAccount, IRepoAccountFilters, IRepoCompanySelection, IRepoCompanySelectionPostResponse } from "../services/service-types/RepoAccountTypes";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IPagedResponse } from "../shared/types/IPagedResponse";
import { IAppState } from "../store/Store";
import { IAuctionAssignmentInsertFromPost } from "./AuctionAssignmentActions";
import {getUser} from "../adalConfig";
// Create Action Constants
export enum RepoAccountActionTypes {
SET_SELECTED_ACCOUNT = "REPOACCOUNT_SET_SELECTED_ACCOUNT",
INSERT_ONE_ACCOUNT = "REPOACCOUNT_INSERT_ONE",
INSERT_ACCOUNTS = "REPOACCOUNT_INSERT_ACCOUNTS",
SET_FILTERED_ACCOUNTS = "REPOACCOUNT_SET_FILTERED_ACCOUNTS",
INSERT_COLLATERAL = "REPOACCOUNT_INSERT_COLLATERAL",
SET_FILTERS = "REPOACCOUNT_SET_FILTERS",
UPDATE_REFUND_STATUS = "REPOACCOUNT_UPDATE_REFUND_STATUS",
SET_SEARCH_PHRASE = "REPOACCOUNT_SET_SEARCH_PHRASE"
}
export interface IRepoAccountSelectAccountAction {
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT;
repoId: IAsyncStateContainer<number>;
}
export interface IRepoAccountSetFilteredAccountsAction {
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS;
filteredAccounts: IAsyncStateContainer<IPagedResponse<number>>;
}
export interface IRepoAccountInsertOneAccount {
type: RepoAccountActionTypes.INSERT_ONE_ACCOUNT;
repoAccount: IRepoAccount;
}
export interface IRepoAccountInsertAccounts {
type: RepoAccountActionTypes.INSERT_ACCOUNTS;
repoAccounts: IRepoAccount[];
}
export interface IRepoAccountInsertCollateral {
type: RepoAccountActionTypes.INSERT_COLLATERAL;
collateral: ICollateral;
}
export interface IRepoAccountSetAccountFiltersAction {
type: RepoAccountActionTypes.SET_FILTERS;
filters: IRepoAccountFilters;
}
export interface IRepoAccountUpdateRefundStatus {
type: RepoAccountActionTypes.UPDATE_REFUND_STATUS;
refundStatus: IRefundStatus;
}
export interface IRepoAccountSetSearchPhraseAction {
type: RepoAccountActionTypes.SET_SEARCH_PHRASE;
searchPhrase: string;
}
/*
Combine the action types with a union (we assume there are more)
example: export type RepoAccountActions = IGetAllAction | IGetOneAction ...
*/
export type RepoAccountActions =
IRepoAccountSelectAccountAction |
IRepoAccountSetFilteredAccountsAction |
IAuctionAssignmentInsertFromPost |
IRepoAccountInsertOneAccount |
IRepoAccountInsertAccounts |
IRepoAccountInsertCollateral |
IRepoAccountSetAccountFiltersAction |
IRepoAccountUpdateRefundStatus |
IRepoAccountSetSearchPhraseAction |
LocationChangeAction;
export const getInitialRepoAccounts: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertAccounts | IRepoAccountSetFilteredAccountsAction | IRepoAccountSetFilteredAccountsAction>> = () => {
return async (dispatch, getState) => {
// Clear filters, as this fetch will not match the current filters
const newFilter: IRepoAccountFilters = {onlyOpenAccounts: getState().repoAccountState.accountFilters.onlyOpenAccounts};
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: getState().repoAccountState.filteredAccounts.state
}
});
await postFilteredAccountsFromURL(Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_filter), newFilter, dispatch);
};
};
export const setSearchPhrase = (searchPhrase: string): IRepoAccountSetSearchPhraseAction => {
return {
type: RepoAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: searchPhrase
};
};
export const goToNextRepoAccountPage: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>> = () => {
return async (dispatch, getState) => {
const currentFilters = getState().repoAccountState.accountFilters;
const filterPageResponse = getState().repoAccountState.filteredAccounts.data;
if (filterPageResponse && filterPageResponse.nextPage) {
await postFilteredAccountsFromURL(filterPageResponse.nextPage, currentFilters, dispatch);
}
};
};
export const goToPreviousRepoAccountPage: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>> = () => {
return async (dispatch, getState) => {
const currentFilters = getState().repoAccountState.accountFilters;
const filterPageResponse = getState().repoAccountState.filteredAccounts.data;
if (filterPageResponse && filterPageResponse.previousPage) {
await postFilteredAccountsFromURL(filterPageResponse.previousPage, currentFilters, dispatch);
}
};
};
export const fetchSelectedRepoAccount: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertOneAccount | IRepoAccountSelectAccountAction>> = (repoId: number) => {
const singleRepoURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single),
{key: "repoId", value: repoId},
);
return async (dispatch) => {
// Since we are fetching the selected account,
// we will need to dispatch both to insert the retrieved
// account, as well as to update the selected account
dispatch({
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT,
repoId: {
state: AsyncState.Loading
}
});
try {
const response = await axios.get(singleRepoURL);
RepoAccountService.ConvertResponseToRepoAccount(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_ONE_ACCOUNT,
repoAccount: response.data
});
dispatch({
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT,
repoId: {
state: AsyncState.Success,
data: response.data.repoId
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Fetch Selected Repo Account",
user: getUser().userName,
url: singleRepoURL,
arguments: repoId
});
dispatch({
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT,
repoId: {
state: AsyncState.Failure
}
});
}
};
};
export const searchRepoAccounts: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetFilteredAccountsAction | IRepoAccountSetAccountFiltersAction | IRepoAccountSetSearchPhraseAction | IRepoAccountInsertAccounts>> = (searchPhrase: string) => {
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_search),
{ key: "searchPhrase", value: searchPhrase }
);
// Since we are fetching the currently filtered accounts,
// we will need to dispatch both to insert the retrieved
// account, as well as to update the currently filtered accounts
return async (dispatch) => {
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Loading
}
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERS,
filters: {}
});
dispatch({
type: RepoAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: searchPhrase
});
try {
const response = await axios.get(parameterizedUrl);
RepoAccountService.ConvertResponseToRepoAccounts(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_ACCOUNTS,
repoAccounts: response.data
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Success,
data: {
items: RepoAccountService.CreateRepoAccountIdMap(response.data),
totalItems: response.data.length,
currentPage: 1,
totalPages: 1
}
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Search Repo Accoutns",
user: getUser().userName,
url: parameterizedUrl,
arguments: searchPhrase
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Failure
}
});
}
};
};
export const removeRepoAccountFromAlertFilter: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetFilteredAccountsAction>> = (alertMessage: string, repoId: number) => {
return async (dispatch, getState) => {
const filteredAccounts = getState().repoAccountState.filteredAccounts;
const accountFilters = getState().repoAccountState.accountFilters;
if (filteredAccounts.data && alertMessage === accountFilters.alertMessage) {
const indexOfEl = filteredAccounts.data.items.indexOf(repoId);
if (indexOfEl >= 0) {
const updatedAccountItems = filteredAccounts.data.items.slice();
updatedAccountItems.splice(indexOfEl, 1);
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
data: {
...filteredAccounts.data,
totalItems: filteredAccounts.data.totalItems - 1,
items: updatedAccountItems
},
state: filteredAccounts.state
}
});
}
}
};
};
export const fetchRepoAccountsViaFilters: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountSetSearchPhraseAction | IRepoAccountInsertAccounts>> =
(filters: IRepoAccountFilters) => {
return async (dispatch) => {
dispatch({
type: RepoAccountActionTypes.SET_FILTERS,
filters: filters
});
dispatch({
type: RepoAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: ""
});
await postFilteredAccountsFromURL(Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_filter), filters, dispatch);
};
};
export const postToNextFilterPage: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>> =
(nextPageUrl: string, filters: IRepoAccountFilters) => {
return async (dispatch: Dispatch<IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>) => {
await postFilteredAccountsFromURL(nextPageUrl, filters, dispatch);
};
};
export const putRepoAccount: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertOneAccount>> = (account: IRepoAccount) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts) + "/" + account.repoId;
return async (dispatch: Dispatch<IRepoAccountInsertOneAccount>) => {
try {
const response = await axios.put(url, account);
RepoAccountService.ConvertResponseToRepoAccount(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_ONE_ACCOUNT,
repoAccount: response.data
});
toast.success("Account Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Repo Account",
user: getUser().userName,
url: url,
arguments: account
});
}
};
};
export const putCollateral: ActionCreator<ThunkAction<Promise<any>, ICollateral, null, IRepoAccountInsertCollateral>> = (collateral: ICollateral) => {
const singleCollateralUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.collateral_single),
{key: "collateralId", value: collateral.collateralId},
);
return async (dispatch) => {
try {
const response = await axios.put(
singleCollateralUrl,
collateral
);
RepoAccountService.ConvertResponseToCollateral(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: response.data
});
toast.success("Collateral Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Collateral",
user: getUser().userName,
url: singleCollateralUrl,
arguments: collateral
});
}
};
};
export const putRepoCompanySelection: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertCollateral>> = (repoCompanySelection: IRepoCompanySelection, collateral: ICollateral) => {
const repoCompanySelectionPutURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_company_selection_single),
{key: "repoCompanySelectionId", value: repoCompanySelection.id},
);
return async (dispatch) => {
try {
const response = await axios.put(
repoCompanySelectionPutURL,
repoCompanySelection
);
RepoAccountService.ConvertResponseToRepoCompanySelection(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: RepoAccountService.CreateCollateralFromRepoCompanySelection(collateral, repoCompanySelection)
});
toast.success("Repo Selection Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Repo Company Selection",
user: getUser().userName,
url: repoCompanySelectionPutURL,
arguments: [repoCompanySelection, collateral]
});
}
};
};
export const postRepoCompanySelection: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertCollateral>> = (repoCompanySelection: IRepoCompanySelection, collateral: ICollateral) => {
const repoCompanySelectionPostURL = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_company_selection);
return async (dispatch) => {
try {
const response = await axios.post(
repoCompanySelectionPostURL,
repoCompanySelection
);
let responseData = response.data as IRepoCompanySelectionPostResponse;
const repoCompanySelectionArray = [
responseData.newRepoCompanySelection
];
RepoAccountService.ConvertResponseToRepoCompanySelection(responseData.newRepoCompanySelection);
if (responseData.previousRepoCompanySelection) {
RepoAccountService.ConvertResponseToRepoCompanySelection(responseData.previousRepoCompanySelection);
repoCompanySelectionArray.push(responseData.previousRepoCompanySelection);
}
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: RepoAccountService.CreateCollateralFromRepoCompanySelection(collateral, repoCompanySelectionArray)
});
toast.success("Repo Selection Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Repo Company Selection",
user: getUser().userName,
url: repoCompanySelectionPostURL,
arguments: [repoCompanySelection, collateral]
});
}
};
};
export const putRefundStatus: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountUpdateRefundStatus>> = (refundStatus: IRefundStatus) => {
const refundStatusPutUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.refund_status_single),
{key: "repoId", value: refundStatus.repoId}
);
return async (dispatch) => {
try {
const response = await axios.put(refundStatusPutUrl, refundStatus);
RepoAccountService.ConvertResponseToRefundStatus(response.data);
dispatch({
type: RepoAccountActionTypes.UPDATE_REFUND_STATUS,
refundStatus: response.data
});
toast.success("Refund Status Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Refund Status",
user: getUser().userName,
url: refundStatusPutUrl,
arguments: refundStatus
});
}
};
};
export const postRefundStatus: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountUpdateRefundStatus>> = (refundStatus: IRefundStatus) => {
const refundStatusPutUrl = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.refund_status);
return async (dispatch) => {
try {
const response = await axios.post(refundStatusPutUrl, refundStatus);
RepoAccountService.ConvertResponseToRefundStatus(response.data);
dispatch({
type: RepoAccountActionTypes.UPDATE_REFUND_STATUS,
refundStatus: response.data
});
toast.success("Refund Status Created.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Refund Status",
user: getUser().userName,
url: refundStatusPutUrl,
arguments: refundStatus
});
}
};
};
export const refreshCollateral: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertCollateral>> = (
collateral: ICollateral, memberNo: number, loanNo: number) => {
const coresCollateralUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixCoreUrl(Configuration.Get.urls.core.loan_collateral),
{key: "loanNo", value: loanNo},
);
return async (dispatch) => {
try {
const singleRepoCollateralUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.collateral_single),
{key: "collateralId", value: collateral.collateralId},
);
const response = await axios.get(coresCollateralUrl);
const updatedCollateral = updateCollateralWithCoreDetails(response.data, collateral);
await axios.put(
singleRepoCollateralUrl,
updatedCollateral,
);
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: updatedCollateral,
});
toast.success("Collateral Refreshed.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Refresh Collateral",
user: getUser().userName,
url: coresCollateralUrl,
arguments: [collateral, memberNo, loanNo]
});
}
};
};
const updateCollateralWithCoreDetails = (data: any[], collateral: ICollateral): ICollateral => {
// eslint-disable-next-line
const found = data.find(c => c.collateralId == collateral.refCollateralId);
if (found.other.miscellaneousInformation != null) {
collateral.vin = found.other.miscellaneousInformation;
}
if (found.vehicle != null) {
collateral.make = found.vehicle.make;
collateral.model = found.vehicle.model;
collateral.vin = found.vehicle.vin;
collateral.year = found.vehicle.year;
}
return collateral;
};
const fetchFilteredAccountsFromAsyncCallback = async (
repoAccountFetch: () => Promise<IPagedResponse<IRepoAccount>>,
dispatch: Dispatch<IRepoAccountInsertAccounts | IRepoAccountSetFilteredAccountsAction>
) => {
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Loading
}
});
try {
const pagedResponse = await repoAccountFetch();
RepoAccountService.ConvertResponseToRepoAccounts(pagedResponse.items);
dispatch({
type: RepoAccountActionTypes.INSERT_ACCOUNTS,
repoAccounts: pagedResponse.items
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Success,
data: {
items: RepoAccountService.CreateRepoAccountIdMap(pagedResponse.items),
totalItems: pagedResponse.totalItems,
nextPage: pagedResponse.nextPage,
previousPage: pagedResponse.previousPage,
currentPage: pagedResponse.currentPage,
totalPages: pagedResponse.totalPages
}
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Fetch Filtered Accounts With Async",
user: getUser().userName,
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Failure
}
});
}
};
const postFilteredAccountsFromURL = async (url: string, filters: IRepoAccountFilters, dispatch: Dispatch<IRepoAccountInsertAccounts | IRepoAccountSetFilteredAccountsAction>) => {
fetchFilteredAccountsFromAsyncCallback(async () => {
const response = await axios.post(url, filters);
return response.data as IPagedResponse<IRepoAccount>;
}, dispatch);
};
@@ -0,0 +1,110 @@
import axios from "axios";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import {ThunkAction} from "redux-thunk";
import { IRepoCompany, IRepoCompanyState } from "../reducers/RepoCompanyReducer";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from "../adalConfig";
export enum RepoCompanyActionTypes {
SET_REPO_COMPANIES = "GET_REPO_COMPANIES",
INSERT_SINGLE_REPO_COMPANY = "INSERT_SINGLE_REPO_COMPANY"
}
export interface IRepoCompanyInsertAction {
type: RepoCompanyActionTypes.SET_REPO_COMPANIES;
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
}
export interface IRepoCompanyInsertSingleAction {
type: RepoCompanyActionTypes.INSERT_SINGLE_REPO_COMPANY;
repoCompany: IRepoCompany;
}
export type RepoCompanyActions = IRepoCompanyInsertAction | IRepoCompanyInsertSingleAction;
export const getAllRepoCompanies: ActionCreator<ThunkAction<Promise<any>, IRepoCompanyState, null, IRepoCompanyInsertAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_companies);
return async (dispatch: Dispatch<IRepoCompanyInsertAction>) =>{
dispatch({
type: RepoCompanyActionTypes.SET_REPO_COMPANIES,
repoCompanies: {
state: AsyncState.Loading,
},
});
try {
const response = await axios.get(url);
dispatch({
type: RepoCompanyActionTypes.SET_REPO_COMPANIES,
repoCompanies: {
data: response.data,
state: AsyncState.Success,
},
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Repo Companies",
user: getUser().userName,
url: url
});
dispatch({
type: RepoCompanyActionTypes.SET_REPO_COMPANIES,
repoCompanies: {
state: AsyncState.Failure,
},
});
}
};
};
export const putRepoCompany: ActionCreator<ThunkAction<Promise<any>, IRepoCompanyState, null, IRepoCompanyInsertSingleAction>> = (repoCompany: IRepoCompany) => {
const parametrizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_companies_single),
{ key: "repoCompanyId", value: repoCompany.id }
);
return async (dispatch: Dispatch<IRepoCompanyInsertSingleAction>) =>{
try {
const response = await axios.put(parametrizedUrl, repoCompany);
const repoCompanyFromServer = response.data as IRepoCompany;
dispatch({
type: RepoCompanyActionTypes.INSERT_SINGLE_REPO_COMPANY,
repoCompany: repoCompanyFromServer
});
toast.success("Repo Company Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Repo Company",
user: getUser().userName,
url: parametrizedUrl,
arguments: repoCompany
});
}
};
};
export const postRepoCompany: ActionCreator<ThunkAction<Promise<any>, IRepoCompanyState, null, IRepoCompanyInsertSingleAction>> = (repoCompany: IRepoCompany) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_companies);
return async (dispatch: Dispatch<IRepoCompanyInsertSingleAction>) =>{
try {
const response = await axios.post(url, repoCompany);
const repoCompanyFromServer = response.data as IRepoCompany;
dispatch({
type: RepoCompanyActionTypes.INSERT_SINGLE_REPO_COMPANY,
repoCompany: repoCompanyFromServer
});
toast.success("Repo Company Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Repo Company",
user: getUser().userName,
url: url,
arguments: repoCompany
});
}
};
};
@@ -0,0 +1,44 @@
import Axios from "axios";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IAdminReportForm, IReport, IReportingState } from "../reducers/ReportingReducer";
import AsyncState from "../shared/enumerations/AsyncState";
import ServerLogging from "../shared/logging/ServerLogging";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from "../adalConfig";
export enum ReportingActionTypes {
SET_REPORT = "SET_REPORT_DATA"
}
export interface IReportingGetReportAction {
type: ReportingActionTypes.SET_REPORT;
reportForm: IAdminReportForm;
currentReport: IAsyncStateContainer<IReport>;
}
export type ReportingActions = IReportingGetReportAction;
export const selectReport: ActionCreator<ThunkAction<Promise<any>, IReportingState, null, IReportingGetReportAction>> = (reportForm: IAdminReportForm) => {
return async (dispatch: Dispatch<IReportingGetReportAction>) => {
if (!reportForm.reportUrl) {
dispatch({
type: ReportingActionTypes.SET_REPORT,
reportForm: reportForm,
currentReport: {
state: AsyncState.Inactive,
},
});
return;
}
dispatch({
type: ReportingActionTypes.SET_REPORT,
reportForm: reportForm,
currentReport: {
state: AsyncState.Loading,
},
});
return;
};
};
@@ -0,0 +1,111 @@
import axios from "axios";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import {ThunkAction} from "redux-thunk";
import { ISavedLocation, ISavedLocationState } from "../reducers/SavedLocationReducer";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from "../adalConfig";
export enum SavedLocationActionTypes {
SET_SAVED_LOCATIONS = "GET_SAVED_LOCATIONS",
INSERT_SINGLE_SAVED_LOCATION = "INSERT_SINGLE_SAVED_LOCATION"
}
export interface ISavedLocationInsertAction {
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS;
savedLocations: IAsyncStateContainer<ISavedLocation[]>;
}
export interface ISavedLocationInsertSingleAction {
type: SavedLocationActionTypes.INSERT_SINGLE_SAVED_LOCATION;
savedLocation: ISavedLocation;
}
export type SavedLocationActions = ISavedLocationInsertAction | ISavedLocationInsertSingleAction;
export const getAllSavedLocations: ActionCreator<ThunkAction<Promise<any>, ISavedLocationState, null, ISavedLocationInsertAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.saved_locations);
return async (dispatch: Dispatch<ISavedLocationInsertAction>) =>{
dispatch({
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS,
savedLocations: {
state: AsyncState.Loading,
},
});
try {
const response = await axios.get(url);
dispatch({
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS,
savedLocations: {
data: response.data,
state: AsyncState.Success,
},
});
} catch (err) {
dispatch({
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS,
savedLocations: {
state: AsyncState.Failure,
},
});
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Saved Locations",
user: getUser().userName,
url: url,
});
}
};
};
export const putSavedLocation: ActionCreator<ThunkAction<Promise<any>, ISavedLocationState, null, ISavedLocationInsertSingleAction>> = (savedLocation: ISavedLocation) => {
const parametrizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.saved_locations_single),
{ key: "savedLocationId", value: savedLocation.id }
);
return async (dispatch: Dispatch<ISavedLocationInsertSingleAction>) =>{
try {
const response = await axios.put(parametrizedUrl, savedLocation);
const savedLocationFromServer = response.data as ISavedLocation;
dispatch({
type: SavedLocationActionTypes.INSERT_SINGLE_SAVED_LOCATION,
savedLocation: savedLocationFromServer
});
toast.success("Saved Location Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Saved Location",
user: getUser().userName,
url: parametrizedUrl,
arguments: savedLocation
});
}
};
};
export const postSavedLocation: ActionCreator<ThunkAction<Promise<any>, ISavedLocationState, null, ISavedLocationInsertSingleAction>> = (savedLocation: ISavedLocation) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.saved_locations);
return async (dispatch: Dispatch<ISavedLocationInsertSingleAction>) =>{
try {
const response = await axios.post(url, savedLocation);
const savedLocationFromServer = response.data as ISavedLocation;
dispatch({
type: SavedLocationActionTypes.INSERT_SINGLE_SAVED_LOCATION,
savedLocation: savedLocationFromServer
});
toast.success("Saved Location Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Saved Location",
user: getUser().userName,
url: url,
arguments: savedLocation
});
}
};
};
@@ -0,0 +1,19 @@
import {UserInfo} from "react-adal";
export enum SecurityActionTypes {
APPLY_USER = 'APPLY_USER',
APPLY_TOKEN = 'APPLY_TOKEN',
}
export interface ISecurityApplyAction {
type: SecurityActionTypes.APPLY_USER;
user: UserInfo;
}
export interface ISecurityTokenAction {
type: SecurityActionTypes.APPLY_TOKEN;
userToken: string;
}
export type SecurityActions = ISecurityApplyAction | ISecurityTokenAction;
@@ -0,0 +1,43 @@
import { ISettingsState } from "../reducers/SettingsReducer";
import { ActionCreator, Dispatch } from "redux";
import {ThunkAction} from 'redux-thunk'
import axios from 'axios';
import Configuration from "../shared/configuration/Configuration";
import ServerLogging from "../shared/logging/ServerLogging";
import {getUser} from "../adalConfig";
export enum SettingsActionTypes {
GET_SETTINGS = 'SETTINGS_GET',
}
export interface ISettingsGetAction {
type: SettingsActionTypes.GET_SETTINGS;
settings: ISettingsState;
}
export type SettingsActions = ISettingsGetAction;
export const getSettings: ActionCreator<ThunkAction<Promise<any>, ISettingsState, null, ISettingsGetAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.settings);
return async (dispatch: Dispatch) => {
try {
const getResponse = await axios.get(url);
const settingsFromServer = getResponse.data as ISettingsState;
settingsFromServer.vehicleTypes.sort((a, b) => a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1);
settingsFromServer.collateralTypes.sort((a, b) => a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1);
settingsFromServer.bodyStyles.sort((a, b) => a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1);
dispatch({
type: SettingsActionTypes.GET_SETTINGS,
settings: settingsFromServer,
})
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Settings",
user: getUser().userName,
url: url,
});
}
}
}
@@ -0,0 +1,74 @@
import { IVehicleFloorPricingParametersState, IVehicleFloorPricingParameters, VehicleFloorPricingParametersPutRequest } from "../reducers/VehicleFloorPricingParametersReducer";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from 'redux-thunk'
import axios from 'axios';
import { toast } from "react-toastify";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import ServerLogging from "../shared/logging/ServerLogging";
import Configuration from "../shared/configuration/Configuration";
import {getUser} from "../adalConfig";
export enum VehicleFloorPricingParametersActionTypes {
GET_VEHICLE_FLOOR_PRICING_PARAMETERS = 'VEHICLE_FLOOR_PRICING_PARAMETERS_GET',
PUT_VEHICLE_FLOOR_PRICING_PARAMETERS = 'VEHICLE_FLOOR_PRICING_PARAMETERS_PUT'
}
export interface IVehicleFloorPricingParametersGetAction {
type: VehicleFloorPricingParametersActionTypes.GET_VEHICLE_FLOOR_PRICING_PARAMETERS;
vehicleFloorPricingParameters: IVehicleFloorPricingParameters[];
}
export interface IVehicleFloorPricingParametersPutAction {
type: VehicleFloorPricingParametersActionTypes.PUT_VEHICLE_FLOOR_PRICING_PARAMETERS;
vehicleFloorPricingParameters: IVehicleFloorPricingParameters[];
}
export type VehicleFloorPricingParametersActions = IVehicleFloorPricingParametersGetAction | IVehicleFloorPricingParametersPutAction;
export const getVehicleFloorPricingParameters: ActionCreator<ThunkAction<Promise<any>, IVehicleFloorPricingParametersState, null, IVehicleFloorPricingParametersGetAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.vehicle_floor_pricing_params);
return async (dispatch: Dispatch) => {
try {
const response = await axios.get(url);
const floorPricingParametersFromServer = response.data;
dispatch({
type: VehicleFloorPricingParametersActionTypes.GET_VEHICLE_FLOOR_PRICING_PARAMETERS,
vehicleFloorPricingParameters: floorPricingParametersFromServer,
})
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Vehicle Floor Pricing Parameters",
user: getUser().userName,
url: url,
});
}
}
}
export const putVehicleFloorPricingParameters: ActionCreator<ThunkAction<Promise<any>, IVehicleFloorPricingParameters[], null, IVehicleFloorPricingParametersPutAction>> = (parameters: IVehicleFloorPricingParameters[]) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.vehicle_floor_pricing_params);
return async (dispatch: Dispatch) => {
try {
var request: VehicleFloorPricingParametersPutRequest = {
vehicleFloorPricingParameters: parameters
};
const response = await axios.put(url, request);
const floorPricingParametersFromServer = response.data.vehicleFloorPricingParameters;
dispatch({
type: VehicleFloorPricingParametersActionTypes.PUT_VEHICLE_FLOOR_PRICING_PARAMETERS,
vehicleFloorPricingParameters: floorPricingParametersFromServer
});
toast.success('Vehicle Floor Parameters Updated.', ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Vehicle Floor Pricing Parameters",
user: getUser().userName,
url: url,
arguments: parameters
});
}
}
}
@@ -0,0 +1,48 @@
import {AuthenticationContext, AdalConfig} from 'react-adal';
export const adalConfig:AdalConfig = {
tenant: '63cd1554-2783-4a86-ae50-b9e6c21c9499',
clientId: '60f721aa-75bf-4195-ae48-d42cbfd8326b',
endpoints: {
api: "cc253cda-05b2-425c-a60c-d1f516a1c4a9"
},
cacheLocation: "localStorage"
};
export const authContext = new AuthenticationContext(adalConfig);
authContext.handleWindowCallback();
// This is an example of getting the token, or the user.
// To get a users groups, it's getUser().profile.roles
export const getToken = () => authContext.getCachedToken(adalConfig.clientId);
export const getUser = () => authContext.getCachedUser();
const hasRole = (profile: { roles?: string[] }, role:string) : boolean => {
if( !profile || !profile.roles || profile.roles.length <= 0 ) {
return false;
}
let hasRole = false;
profile.roles.forEach( (roleInput) => {
if( roleInput.toLowerCase() === role.toLowerCase() ) {
hasRole = true;
}
});
return hasRole;
};
export const hasAdminRole = () => {
const user = getUser();
return user && hasRole( user.profile, "Admin");
};
export const hasAnyRole = () => {
const user = getUser();
return user && (hasRole( user.profile, "Admin") || hasRole(user.profile, "Specialist"));
};
/*
export const adalApiFetch = (fetch: any, url: string, options: any) =>
adalFetch(authContext, adalConfig.endpoints.api, fetch, url, options);
export const withAdalLoginApi = withAdalLogin(authContext, adalConfig.endpoints.api);
*/
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -0,0 +1,120 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import * as adalConfig from "../../adalConfig";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AccountPageHeader from "./AccountPageHeader";
describe("AccountPageHeader", () => {
var spy: jest.SpyInstance;
let wrapper: ShallowWrapper;
const selectedAccount: IRepoAccount = {
repoId: 476,
origAccountTypeCode: "",
accountNo: "123456",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 0,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
currentDelinquentAmount: 19.45,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [{
repoId: 1,
individualId: "1",
ownershipType: 6,
individualName: "John Doe",
}],
collaterals: [],
repoStatusChanges: [],
};
let updatedBackButtonValue: boolean;
beforeEach(() => {
updatedBackButtonValue = false;
wrapper = shallow(
<AccountPageHeader
accountNoHeader="abcd1234"
selectedRepoAccount={selectedAccount}
allowedStatusChanges={[]}
onStatusChange={() => {}}
onBackButtonClicked={() => updatedBackButtonValue = true}
putRepoAccount={()=>{}}
/>
);
spy = jest.spyOn(adalConfig, "hasAdminRole").mockReturnValueOnce(false).mockReturnValueOnce(true);
});
it ("Renders Member Name", () => {
const memberName = wrapper.find(`[data-test='account-page-header-member-name']`);
expect(memberName.length).toBe(1);
expect(memberName.text()).toEqual("John Doe");
});
it ("Renders Missing When There is No Selected Account", () => {
wrapper.setProps({
selectedRepoAccount: undefined,
});
const memberName = wrapper.find(`[data-test='account-page-header-member-name']`);
expect(memberName.length).toBe(1);
expect(memberName.text()).toEqual("Missing");
});
it ("Back Button Callback Works", () => {
const backBtn = wrapper.find(`[data-test='account-page-header-back-btn']`);
expect(backBtn.length).toBe(1);
expect(updatedBackButtonValue).toBe(false);
backBtn.simulate("click");
expect(updatedBackButtonValue).toBe(true);
});
it ("Does Not Render \"(Closed)\" Next to Name When Account is Open", () => {
const closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
expect(closedAccountLabel.length).toBe(0);
});
it ("Renders \"(Closed)\" Next to Name When Account is Closed", () => {
wrapper = wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
dateClosed: moment("1941-12-07")
}
});
var closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
expect(closedAccountLabel.length).toBe(1);
closedAccountLabel = wrapper.find(`[data-test='reopen-button']`);
expect(closedAccountLabel.length).toBe(0);
});
it ("Renders \"Repopen\" button Next to Name When Account is Closed and user is Admin", () => {
wrapper = wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
dateClosed: moment("1941-12-07")
}
});
var closedAccountLabel = wrapper.find(`[data-test='closed-account-label']`);
expect(closedAccountLabel.length).toBe(1);
closedAccountLabel = wrapper.find(`[data-test='reopen-button']`);
expect(closedAccountLabel.length).toBe(1);
});
});
@@ -0,0 +1,78 @@
import React from 'react';
import { IAllowedStatusChange } from '../../reducers/SettingsReducer';
import StatusChangeSelector from '../status-change-selector/StatusChangeSelector';
import RepoAccountService from '../../services/RepoAccountService';
import { IRepoAccount } from '../../services/service-types/RepoAccountTypes';
import * as adalConfig from "../../adalConfig";
const cloneDeep = require("lodash.clonedeep");
interface AccountPageHeaderProps {
accountNoHeader: string;
selectedRepoAccount?: IRepoAccount;
allowedStatusChanges: IAllowedStatusChange[];
onBackButtonClicked: () => any;
onStatusChange: (status: string) => any;
putRepoAccount: (repoAccount: IRepoAccount) => any;
}
const AccountPageHeader: React.FC<AccountPageHeaderProps> = (props) => {
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedRepoAccount);
const reopenAccountClicked = () => {
const shouldReOpenAccount = window.confirm("Are you sure you want to reopen this account? This can have unexpected side effects. Use Caution with accounts that have already been refunded.");
if(shouldReOpenAccount) {
reopenClosedAccount();
}
}
const reopenClosedAccount = () => {
if (!props.selectedRepoAccount) {
console.warn("Cannot close account when the Selected Account is not loaded. Returning.");
return;
}
const copiedRepoAccount = cloneDeep(props.selectedRepoAccount) as IRepoAccount;
copiedRepoAccount.dateClosed = undefined;
props.putRepoAccount(copiedRepoAccount);
}
return (
<div className={'back-navigation-header' + (isAccountClosed ? ' header-closed' : '')}>
<div className="row form-inline">
<div className="col-sm">
<h5>
<i className="fa fa-chevron-left text-primary" aria-hidden="true" onClick={props.onBackButtonClicked} data-test="account-page-header-back-btn"></i>
Account: { props.accountNoHeader }
{ isAccountClosed ?
(
<span className="text-danger" data-test="closed-account-label"> (CLOSED)</span>
)
: null}
{ isAccountClosed && adalConfig.hasAdminRole() ?
(
<button className="form-input btn btn-danger btn-sm ml-2" data-test="reopen-button" onClick={() => reopenAccountClicked()}>Reopen This Account</button>
)
: null }
</h5>
</div>
<div className="col-sm">
<p><b data-test="account-page-header-member-name">{RepoAccountService.GetMemberName(props.selectedRepoAccount)}</b></p>
</div>
<div className="col-sm">
<StatusChangeSelector
allowedStatusChanges={props.allowedStatusChanges}
onStatusChange={props.onStatusChange}
selectedRepoAccount={props.selectedRepoAccount}
/>
</div>
</div>
<hr></hr>
</div>
);
}
export default AccountPageHeader;
@@ -0,0 +1,8 @@
.alerts-list {
max-height: 11em;
overflow-y: scroll;
}
.alert-box .card-header {
padding: .5em;
}
@@ -0,0 +1,140 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import ReactDOM from "react-dom";
import { IAlert } from "../../reducers/AlertReducer";
import AlertBox, { groupAlerts, IAlertBoxProps } from "./AlertBox";
it("renders without crashing", () => {
const div = document.createElement("div");
const props: IAlertBoxProps = { alerts: [], title: "title", alertRowClicked: () => {}};
ReactDOM.render(<AlertBox {...props} />, div);
ReactDOM.unmountComponentAtNode(div);
});
const findByTestAttribute = (wrapper: ShallowWrapper, attr: string) => {
const component = wrapper.find(`[data-test='${attr}']`);
return component;
};
describe("AlertBox Component", () => {
const props: IAlertBoxProps = { alerts: [], title: "title", alertRowClicked: () => {}};
let wrapper = shallow(<AlertBox {...props} />);
const first: IAlert = { id: 1, repoId: 2, accountNo: "2", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Message", dayCount: 60 };
const second: IAlert = { id: 2, repoId: 3, accountNo: "2", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Jane Sample", markedReadBy: "", message: "Message", dayCount: 60 };
const third: IAlert = { id: 3, repoId: 4, accountNo: "3", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Other Message", dayCount: 3 };
const fourth: IAlert = { id: 4, repoId: 5, accountNo: "4", dateAdded: moment(), dateMarkedRead: moment(), createdBy: "Joe Sample", markedReadBy: "", message: "Other Message", dayCount: 3 };
it ("Should render without errors", () => {
const component = findByTestAttribute(wrapper, "alertBoxComponent");
expect(component.length).toBe(1);
});
it ("Should render a header", () => {
const component = findByTestAttribute(wrapper, "alertBoxHeader");
expect(component.length).toBe(1);
});
it ("Should render an alerts list", () => {
const component = findByTestAttribute(wrapper, "alertsList");
expect(component.length).toBe(1);
});
it("groups an empty alert array", () => {
// arrange
const alerts: IAlert[] = [];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(0);
});
it("groups one alert", () => {
// arrange
const alerts: IAlert[] = [first];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(1);
const group = result[0];
expect(group.message).toBe("Message");
expect(group.repoIds[0]).toBe(2);
});
it("groups two alerts of the same type", () => {
// arrange
const alerts: IAlert[] = [first, second];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(1);
const group = result[0];
expect(group.message).toBe("Message");
expect(group.repoIds[0]).toBe(2);
expect(group.repoIds[1]).toBe(3);
});
it("groups two alerts of different types", () => {
// arrange
const alerts: IAlert[] = [first, third];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(2);
const messageGroup = result[0];
expect(messageGroup.message).toBe("Message");
expect(messageGroup.repoIds[0]).toBe(2);
const otherGroup = result[1];
expect(otherGroup.message).toBe("Other Message");
expect(otherGroup.repoIds[0]).toBe(4);
});
it("discards duplicate alerts", () => {
// arrange
const alerts: IAlert[] = [first, second, first];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(1);
const group = result[0];
expect(group.message).toBe("Message");
expect(group.repoIds[0]).toBe(2);
expect(group.repoIds[1]).toBe(3);
});
it("sorts alert groups in descending order of alert count", () => {
// arrange
const alerts: IAlert[] = [first, third, fourth];
// act
const result = groupAlerts(alerts);
// assert
expect(result.length).toBe(2);
const otherGroup = result[0];
expect(otherGroup.message).toBe("Other Message");
expect(otherGroup.repoIds[0]).toBe(4);
expect(otherGroup.repoIds[1]).toBe(5);
const messageGroup = result[1];
expect(messageGroup.message).toBe("Message");
expect(messageGroup.repoIds[0]).toBe(2);
});
});
@@ -0,0 +1,66 @@
import React from "react";
import { IAlert } from "../../reducers/AlertReducer";
import IAlertGroup from "../../shared/types/IAlertGroup";
import AlertRow from "./../alert-row/AlertRow";
import "./AlertBox.css";
export interface IAlertBoxProps {
alerts: IAlert[];
currentlySelectedAlertMessage?: string;
title: string;
alertRowClicked: (message: string, repoIds: number[]) => any;
}
const AlertBox: React.FC<IAlertBoxProps> = (props: IAlertBoxProps) => {
const groups = groupAlerts(props.alerts);
return (
<div className="card h-100 alert-box" data-test="alertBoxComponent">
<h6 className="card-header" data-test="alertBoxHeader">
{totalAlertCount(groups)} {props.title}
</h6>
<div className="">
<ul className="alerts-list list-group list-group-flush" data-test="alertsList">
{groups.map((value, key) => {
return (
<AlertRow
key={key}
message={value.message}
currentlySelectedAlertMessage={props.currentlySelectedAlertMessage}
repoIds={value.repoIds}
alertRowClicked={props.alertRowClicked}
/>
);
})}
</ul>
</div>
</div>
);
};
const totalAlertCount = (groups: IAlertGroup[]) => {
return groups.reduce((total, group) => total + group.repoIds.length, 0);
};
export const groupAlerts = (alerts: IAlert[]) => {
let groups: IAlertGroup[] = [];
for (let alert of alerts) {
let found = groups.find(x => x.message === alert.message);
if (!found) {
var group: IAlertGroup = {message: alert.message, repoIds: [alert.repoId]};
groups.push(group);
} else {
if (!found.repoIds.includes(alert.repoId)){
found.repoIds.push(alert.repoId);
}
}
}
return groups.sort((a, b) => {
return b.repoIds.length - a.repoIds.length;
});
};
export default AlertBox;
@@ -0,0 +1,13 @@
.alert-row {
cursor: pointer;
font-size: .9em;
padding: .5em 1em;
}
.alert-row.selected-row {
background-color: #d9ebff;
}
.alert-row:hover:not(.selected-row) {
background-color: #f5f5f5;
}
@@ -0,0 +1,90 @@
import { AssertionError } from "assert";
import { shallow, ShallowWrapper } from "enzyme";
import React, { Component } from "react";
import AlertRow from "./AlertRow";
const findByTestAttribute = (component: ShallowWrapper, attr: string) => {
const wrapper = component.find(`[data-test='${attr}']`);
return wrapper;
};
describe("AlertRow", () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[]}
alertRowClicked={(_: string, __: number[]) => {}}
/>
);
});
it ("Should render without errors", () => {
const component = findByTestAttribute(wrapper, "alertRowComponent");
expect(component.length).toBe(1);
});
it ("Should render an alert type", () => {
const component = findByTestAttribute(wrapper, "alertType");
expect(component.length).toBe(1);
});
it ("Should render a type count", () => {
const component = findByTestAttribute(wrapper, "typeCount");
expect(component.length).toBe(1);
});
it("Should Render as badge-primary with count under 5", () => {
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3]}
alertRowClicked={(_: string, __: number[]) => {}}
/>);
const component = findByTestAttribute(tempwrapper, "typeCount");
expect(component.hasClass("badge-primary")).toBe(true);
});
it("Should Render as badge-warning with count > 6 and count < 20", () => {
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3,4,5,6,7,8]}
alertRowClicked={(_: string, __: number[]) => {}}
/>);
const component = findByTestAttribute(tempwrapper, "typeCount");
expect(component.hasClass("badge-warning")).toBe(true);
});
it("Should Render as badge-danger when count is greater than 20", () => {
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]}
alertRowClicked={(_: string, __: number[]) => {}}
/>);
const component = findByTestAttribute(tempwrapper, "typeCount");
expect(component.hasClass("badge-danger")).toBe(true);
});
it("alert row properly handles click", () => {
var clicked = false;
var tempwrapper = shallow(
<AlertRow
message="Cras justo odio"
repoIds={[1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]}
alertRowClicked={(_: string, __: number[]) => {clicked=true}}
/>);
const component = findByTestAttribute(tempwrapper, "alertRowComponent");
component.simulate("click");
expect(clicked).toBe(true);
});
});
@@ -0,0 +1,39 @@
import React from "react";
import "./AlertRow.css";
interface IAlertRowProps {
message: string;
repoIds: number[];
currentlySelectedAlertMessage?: string;
alertRowClicked: (message: string, repoIds: number[]) => any;
}
const AlertRow: React.FC<IAlertRowProps> = (props: IAlertRowProps) => {
const rowClicked = () => {
props.alertRowClicked(props.message, props.repoIds);
};
const getCountClassStyle = (count: number): string => {
if (count < 5) {
return "badge-primary";
}
if (count < 20) {
return "badge-warning";
}
return "badge-danger";
};
return (
<li
className={"alert-row list-group-item d-flex justify-content-between align-items-center " + (props.currentlySelectedAlertMessage === props.message ? " selected-row" : "")}
data-test="alertRowComponent"
onClick={rowClicked}
>
<span data-test="alertType">{props.message}</span>
<span className={"badge badge-pill " + getCountClassStyle(props.repoIds.length)} data-test="typeCount">{props.repoIds.length}</span>
</li>
);
};
export default AlertRow;
@@ -0,0 +1,9 @@
.auction-assignment-section input,
.auction-assignment-section select,
.auction-assignment-section textarea {
margin-bottom: 0.5em;
}
.auction-assignment-section p {
margin-bottom: 0;
}
@@ -0,0 +1,181 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import { ICleaningLevel } from "../../reducers/SettingsReducer";
import AuctionAssignment, { IUserSelectedAuctionAssignment } from "./AuctionAssignment";
// - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
describe("Auction Collateral Location Component", () => {
let wrapper: ShallowWrapper;
const dummyAuctionAssignment: IUserSelectedAuctionAssignment = { repoId: 1 };
const dummyCleaningLevels: ICleaningLevel[] = [
{
code: 1,
description: "Super Clean",
},
{
code: 2,
description: "Dirty",
},
];
const dummyAuctionHouses: IAuctionHouse[] = [
{
auctionID: "1060",
name: "Dealers Auto Auction Southwest",
active: "yes",
address_Line_1: "1433 South 19th Avenue",
address_Line_2: "-",
city: "Phoenix",
state: "AZ",
zip: 85009,
country: "USA",
phone: "(602) 253-7766",
fax: "(602) 253-4646",
latitude: 33.4337272644043,
longitude: -112.09916687011719
},
{
auctionID: "1061",
name: "ADESA Portland",
active: "yes",
address_Line_1: "23585 NE Sandy Blvd.",
address_Line_2: "-",
city: "Wood Village",
state: "OR",
zip: 97060,
country: "USA",
phone: "(800) 300-3200",
fax: "(503) 661-7286",
latitude: 45.545101165771484,
longitude: -122.41893005371094
},
];
let dummyAuctionAssignmentToUpdate: IUserSelectedAuctionAssignment = { repoId: 1 };
const dummyUpdateAuctionAssignment = (auctionAssignment: IUserSelectedAuctionAssignment) => {
dummyAuctionAssignmentToUpdate = auctionAssignment;
};
beforeEach(() => {
wrapper = shallow(
<AuctionAssignment
auctionHouses={dummyAuctionHouses}
cleaningLevels={dummyCleaningLevels}
auctionAssignment={dummyAuctionAssignment}
onAuctionAssignmentChange={dummyUpdateAuctionAssignment}
collateralLocation={{} as any}
/>
);
});
afterEach(() => {
dummyAuctionAssignmentToUpdate = {
repoId: 1
};
});
const getCardView = () => {
return wrapper.find("CardView").dive();
};
it("Renders & Updates Auction House", () => {
// Inital value is present
let auctionHouseSelection = getCardView().find(`[data-test='auction-assignment-auction-house']`);
let auctionHouseAddress = getCardView().find(`[data-test='auction-assignment-auction-house-address']`);
expect(auctionHouseSelection.length).toBe(1);
expect(auctionHouseSelection.props().value).toBe(undefined);
expect(auctionHouseAddress.length).toBe(1);
expect(auctionHouseAddress.props().value).toEqual("Select Auction House for Address");
// Change triggers callback
auctionHouseSelection.simulate("change", "1060");
expect(dummyAuctionAssignmentToUpdate.auctionId).toEqual(dummyAuctionHouses[0].auctionID);
// Change does not alter original prop object
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
// Prop changes update input
wrapper = wrapper.setProps({
auctionAssignment: {
auctionId: dummyAuctionHouses[1].auctionID,
},
});
auctionHouseSelection = getCardView().find(`[data-test='auction-assignment-auction-house']`);
auctionHouseAddress = getCardView().find(`[data-test='auction-assignment-auction-house-address']`);
expect(auctionHouseSelection.props().value).toBe("1061");
expect(auctionHouseAddress.props().value).toEqual("23585 NE Sandy Blvd. -, Wood Village, OR, USA 97060");
});
it("Renders & Updates Cleaning Level", () => {
// Inital value is present
let cleaningLevelSelection = getCardView().find(`[data-test='auction-assignment-cleaning-level']`);
expect(cleaningLevelSelection.length).toBe(1);
expect(cleaningLevelSelection.props().value).toBeUndefined();
// Change triggers callback
cleaningLevelSelection.simulate("change", 1);
expect(dummyAuctionAssignmentToUpdate.cleaningLevel).not.toBeUndefined();
expect(dummyAuctionAssignmentToUpdate.cleaningLevel).toEqual(dummyCleaningLevels[0].code);
// Change does not alter original prop object
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
// Prop changes update input
wrapper = wrapper.setProps({
auctionAssignment: {
cleaningLevel: dummyCleaningLevels[1].code,
},
});
cleaningLevelSelection = getCardView().find(`[data-test='auction-assignment-cleaning-level']`);
expect(cleaningLevelSelection.props().value).toBe(2);
});
it("Renders & Updates Auction Notes", () => {
// Inital value is present
let additionalNotesInput = getCardView().find(`[data-test='auction-assignment-pickup-notes']`);
expect(additionalNotesInput.length).toBe(1);
expect(additionalNotesInput.props().value).toBeUndefined();
// Change triggers callback
additionalNotesInput.simulate("change", "Some New Notes");
expect(dummyAuctionAssignmentToUpdate.auctionNotes).not.toBeUndefined();
expect(dummyAuctionAssignmentToUpdate.auctionNotes).toEqual("Some New Notes");
// Change does not alter original prop object
expect(dummyAuctionAssignment).not.toBe(dummyAuctionAssignmentToUpdate);
expect(dummyAuctionAssignment).toEqual({ repoId: 1 });
// Prop changes update input
wrapper = wrapper.setProps({
auctionAssignment: {
auctionNotes: "Even newer Notes!",
},
});
additionalNotesInput = getCardView().find(`[data-test='auction-assignment-pickup-notes']`);
expect(additionalNotesInput.props().value).toBe("Even newer Notes!");
});
it("Sets Default Legal Sale Date When Legal Sale Date is Undefined", () => {
wrapper.setProps({
defaultLegalSaleDate: moment("1969-07-20")
});
expect(dummyAuctionAssignmentToUpdate.legalSaleDate).toEqual(moment("1969-07-20"));
});
it("Does Not Set Default Legal Sale Date When Legal Sale Date is Not Undefined", () => {
wrapper.setProps({
defaultLegalSaleDate: moment("1969-07-20"),
auctionAssignment: {
legalSaleDate: moment("1957-10-04")
}
});
expect(dummyAuctionAssignmentToUpdate.legalSaleDate).toEqual(undefined);
});
});
@@ -0,0 +1,144 @@
import moment, { Moment } from "moment";
import React from "react";
import { ICollateralLocation } from "../../containers/auction-collateral-location/AuctionCollateralLocation";
import { IAuctionHouse } from "../../reducers/AuctionHouseReducer";
import { ICleaningLevel } from "../../reducers/SettingsReducer";
import CardView from "../card-view/CardView";
import FormInput, { FormInputType } from "../form-input/FormInput";
import AuctionAssignmentMap from "./auction-assignment-map/AuctionAssignmentMap";
import "./AuctionAssignment.css";
const cloneDeep = require("lodash.clonedeep");
export interface IUserSelectedAuctionAssignment {
repoId: number;
auctionId?: string;
cleaningLevel?: number;
availablePickupDate?: Moment;
legalSaleDate?: Moment;
auctionNotes?: string;
}
export interface IAuctionAssignmentProps {
collateralLocation: ICollateralLocation;
auctionAssignment: IUserSelectedAuctionAssignment;
auctionHouses: IAuctionHouse[];
cleaningLevels: ICleaningLevel[];
defaultLegalSaleDate?: Moment;
onAuctionAssignmentChange: (auctionAssignment: IUserSelectedAuctionAssignment) => any;
}
type IUserAuctionAssignmentUpdater = (x: IUserSelectedAuctionAssignment) => void;
const AuctionAssignment: React.FC<IAuctionAssignmentProps> = (props) => {
const applyChange = (updater: IUserAuctionAssignmentUpdater) => {
const copiedAuctionassignment = cloneDeep(props.auctionAssignment);
updater(copiedAuctionassignment);
props.onAuctionAssignmentChange(copiedAuctionassignment);
};
const getAuctionHouseAddressDisplay = (auctionId?: string) => {
if (!auctionId) {
return "Select Auction House for Address";
}
const auctionHouse = props.auctionHouses.find((auction) => auction.auctionID === auctionId );
if (auctionHouse) {
return `${auctionHouse.address_Line_1} ${auctionHouse.address_Line_2}, ${auctionHouse.city}, ${auctionHouse.state}, ${auctionHouse.country} ${auctionHouse.zip || ""}`;
}
};
const setDefaultLegalSaleValue = (defaultLegalSale?: Moment) => {
if (!props.auctionAssignment.legalSaleDate && defaultLegalSale) {
applyChange(auctionAssignment => auctionAssignment.legalSaleDate = defaultLegalSale);
}
};
setDefaultLegalSaleValue(props.defaultLegalSaleDate);
return (
<CardView
header="Auction Assignment"
rows={[
<div key="auction-assignment-form" className="auction-assignment-section container">
<div className="row">
<div className="col-md">
<FormInput
label="Auction House"
type={FormInputType.Selection}
value={props.auctionAssignment.auctionId}
onChange={(auctionId) => applyChange(auctionAssignment => auctionAssignment.auctionId = auctionId)}
required={true}
placeholder="Select Auction House"
options={props.auctionHouses.map((auctionHouse) => {
return {
value: auctionHouse.auctionID,
display: auctionHouse.name
};
})}
data-test="auction-assignment-auction-house"
/>
<input
className="form-control"
value={getAuctionHouseAddressDisplay(props.auctionAssignment.auctionId)}
data-test="auction-assignment-auction-house-address"
disabled={true}
/>
<hr/>
<FormInput
label="Cleaning Level"
type={FormInputType.Selection}
value={props.auctionAssignment.cleaningLevel}
onChange={(cleaningLevel) => applyChange(auctionAssignment => auctionAssignment.cleaningLevel = parseInt(cleaningLevel))}
placeholder="Select Cleaning Level"
required={true}
options={props.cleaningLevels.map((cleaningLevel) => {
return {
value: cleaningLevel.code,
display: cleaningLevel.description
};
})}
data-test="auction-assignment-cleaning-level"
/>
<FormInput
label="Available For Pickup"
type={FormInputType.Datetime}
minDate={moment()}
value={props.auctionAssignment.availablePickupDate}
required={true}
onChange={(availablePickupDate) => applyChange(auctionAssignment => auctionAssignment.availablePickupDate = availablePickupDate)}
data-test="auction-assignment-available-for-pickup"
/>
<FormInput
label="Legal Sale"
type={FormInputType.Datetime}
minDate={moment()}
value={props.auctionAssignment.legalSaleDate}
required={true}
onChange={(legalSaleDate) => applyChange(auctionAssignment => auctionAssignment.legalSaleDate = legalSaleDate)}
data-test="auction-assignment-legal-sale"
/>
<FormInput
label="Additional Pickup Notes"
type={FormInputType.Textarea}
value={props.auctionAssignment.auctionNotes}
onChange={(auctionNotes) => applyChange(auctionAssignment => auctionAssignment.auctionNotes = auctionNotes)}
data-test="auction-assignment-pickup-notes"
/>
</div>
<div className="col-md">
<AuctionAssignmentMap
collateralLocation={props.collateralLocation}
currentlySelectedAuctionId={props.auctionAssignment.auctionId}
auctionHouses={props.auctionHouses}
onPinSelected={(auctionId) => applyChange(auctionAssignment => auctionAssignment.auctionId = auctionId)}
data-test=""
/>
</div>
</div>
</div>
]}
/>
);
};
export default AuctionAssignment;
@@ -0,0 +1,9 @@
#auction-house-location-map {
width: 100%;
height: 100%;
min-height: 320px;
}
#collateral-center-btn {
margin-top: 0.5em;
}
@@ -0,0 +1,11 @@
import { shallow, ShallowWrapper, mount} from "enzyme";
import moment from "moment";
import React from "react";
import AuctionAssignmentMap from "./AuctionAssignmentMap";
//import AuctionAssignment, { IUserSelectedAuctionAssignment } from "./AuctionAssignment";
//TODO: This one is hard, come back to this later
describe("Auction Assignment Map Component", () => {
it("Does Nothing For Now", () => {});
});
@@ -0,0 +1,244 @@
import React from "react";
import * as atlas from 'azure-maps-control';
import { toast } from "react-toastify";
import { ICollateralLocation } from "../../../containers/auction-collateral-location/AuctionCollateralLocation";
import { IAuctionHouse } from "../../../reducers/AuctionHouseReducer";
import { ILocationCoordinates } from "../../../services/service-types/LocationTypes";
import NetworkService from "../../../services/NetworkService";
import Configuration from "../../../shared/configuration/Configuration";
import ToastDefaults from "../../../shared/notifications/ToastDefaults";
import "./AuctionAssignmentMap.css";
//const Microsoft = (window as any).Microsoft;
var map: atlas.Map;
var popup = new atlas.Popup({
pixelOffset: [0, -18],
closeButton: false
});
interface IAuctionAssignmentMapProps {
collateralLocation: ICollateralLocation;
auctionHouses: IAuctionHouse[];
currentlySelectedAuctionId?: string;
onPinSelected: (auctionId: string) => any;
}
interface ICoordinates {
lat: number;
long: number;
}
interface IAuctionAssignmentMapState {
collateralCoordinates?: ICoordinates;
}
class AuctionAssignmentMap extends React.Component<IAuctionAssignmentMapProps, IAuctionAssignmentMapState> {
constructor(props: IAuctionAssignmentMapProps) {
super(props);
this.state = {};
this.LocationsApiRecieved = this.LocationsApiRecieved.bind(this);
this.centerMap = this.centerMap.bind(this);
}
componentDidMount() {
this.instantiateMap();
}
componentDidUpdate(prevProps: IAuctionAssignmentMapProps, prevState: IAuctionAssignmentMapState) {
if (this.state.collateralCoordinates && prevState.collateralCoordinates !== this.state.collateralCoordinates) {
this.updateMap(true);
}
if (prevProps.currentlySelectedAuctionId !== this.props.currentlySelectedAuctionId) {
this.updateMap();
}
}
centerMap() {
if (this.isCollateralLocationFilled()) {
const addressParam = encodeURIComponent(this.props.collateralLocation.address);
const zipParam = encodeURIComponent(this.props.collateralLocation.zip);
const url = `${Configuration.Get.baseUrls.locationGeocode}api/Location/GetClosestPositionByAddress?Street=${addressParam}&ZipCode=${zipParam}`;
this.sendLocationsApiRequest(url);
}
}
isCollateralLocationFilled() {
return !!this.props.collateralLocation.address && !!this.props.collateralLocation.zip;
}
sendLocationsApiRequest(url: string) {
NetworkService.FetchLocationData(url)
.then((data) => {
this.LocationsApiRecieved(data);
})
.catch((error) => {
const message = "Could not locate collateral address";
window.console.warn(message, error);
toast.warn(message, ToastDefaults.baseParams);
});
}
LocationsApiRecieved(data: ILocationCoordinates) {
this.setState({
collateralCoordinates: {
lat: data.latitude,
long: data.longitude
}
});
}
getAuctionHouse(auctionId?: string) {
return this.props.auctionHouses.find((auctionHouse) => auctionHouse.auctionID === auctionId);
}
updateMap(switchViewToCollateral: boolean = false) {
popup.close()
const selectedAuctionHouse = this.getAuctionHouse(this.props.currentlySelectedAuctionId);
let viewPortLocation: any;
if (this.state.collateralCoordinates && switchViewToCollateral) {
viewPortLocation = atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat);//test this Corey
} else if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
viewPortLocation = atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude, selectedAuctionHouse.latitude);
} else {
viewPortLocation = atlas.data.Position.fromLatLng(
Configuration.Get.defaultAuctionAssignmentMapLocation.longitude,
Configuration.Get.defaultAuctionAssignmentMapLocation.latitude,
);
}
map.markers.clear();
map.setCamera({
center: viewPortLocation
})
var validPins = [];
// Push Collateral & Selected Auction Pins First
// This will ensure that they appear on top
if (this.state.collateralCoordinates) {
validPins.push(new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat),
color: "green"
}));
}
if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
const selectedAuctionHouse = this.props.auctionHouses.find((auctionHouse) => auctionHouse.auctionID === this.props.currentlySelectedAuctionId);
if (selectedAuctionHouse) {
const pushPin = new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude || 0, selectedAuctionHouse.latitude || 0),//Check this Corey
color: "red"
});
validPins.push(pushPin);
map.events.add("mouseover", pushPin, () => this.openPopup(selectedAuctionHouse, popup, map));
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave
}
}
for (let i = 0; i < this.props.auctionHouses.length; i++) {
const auctionHouse = this.props.auctionHouses[i];
if (auctionHouse.latitude && auctionHouse.longitude && this.props.currentlySelectedAuctionId !== auctionHouse.auctionID) {
const pushPin = new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(auctionHouse.longitude, auctionHouse.latitude),
});
validPins.push(pushPin);
map.events.add("mouseover", pushPin, () => this.openPopup(auctionHouse, popup, map));
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave)
map.events.add("click", pushPin, () => this.props.onPinSelected(auctionHouse.auctionID));
}
}
map.markers.add(validPins);
}
instantiateMap(switchViewToCollateral: boolean = false) {
const selectedAuctionHouse = this.getAuctionHouse(this.props.currentlySelectedAuctionId);
let viewPortLocation: any;
if (this.state.collateralCoordinates && switchViewToCollateral) {
viewPortLocation = atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat);//test this Corey
} else if (selectedAuctionHouse && selectedAuctionHouse.latitude && selectedAuctionHouse.longitude) {
viewPortLocation = atlas.data.Position.fromLatLng(selectedAuctionHouse.longitude, selectedAuctionHouse.latitude);
} else {
viewPortLocation = atlas.data.Position.fromLatLng(
Configuration.Get.defaultAuctionAssignmentMapLocation.longitude,
Configuration.Get.defaultAuctionAssignmentMapLocation.latitude,
);
}
map = new atlas.Map("auction-house-location-map", {
zoom: 9,
center: viewPortLocation,
authOptions: {
authType: atlas.AuthenticationType.subscriptionKey,
subscriptionKey: Configuration.Get.atlasCredentials
}
});
var validPins = [];
// Push Collateral & Selected Auction Pins First
// This will ensure that they appear on top
if (this.state.collateralCoordinates) {
validPins.push(new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(this.state.collateralCoordinates.long, this.state.collateralCoordinates.lat),
color: "green"
}));
}
for (let i = 0; i < this.props.auctionHouses.length; i++) {
const auctionHouse = this.props.auctionHouses[i];
if (auctionHouse.latitude && auctionHouse.longitude && this.props.currentlySelectedAuctionId !== auctionHouse.auctionID) {
const pushPin = new atlas.HtmlMarker({
position: atlas.data.Position.fromLatLng(auctionHouse.longitude, auctionHouse.latitude),
});
validPins.push(pushPin);
map.events.add("mouseover", pushPin, () => this.openPopup(auctionHouse, popup, map));
map.events.add("mouseleave", pushPin, () => popup.close()); // Clear popup on mouse leave)
map.events.add("click", pushPin, () => this.props.onPinSelected(auctionHouse.auctionID));
}
}
map.markers.add(validPins);
}
openPopup(auctionHouse: IAuctionHouse, popup: atlas.Popup, map: atlas.Map) {
console.log("Mouseover on pin for auction house:" + auctionHouse.name, auctionHouse.city, auctionHouse.longitude, auctionHouse.latitude);
if (!map) {
console.log("Map is not initialized, cannot open popup.")
}
popup.setOptions({
content: "<div>" + auctionHouse.name + "<br>" + auctionHouse.city + "</div>",
position: [auctionHouse.longitude||0, auctionHouse.latitude||0],
})
popup.open(map);
}
render() {
return (
<div>
<div id="auction-house-location-map" />
<button
id="collateral-center-btn"
type="button"
className="btn btn-primary"
disabled={!this.isCollateralLocationFilled()}
onClick={this.centerMap}
>
Center on Collateral
</button>
</div>
);
}
}
export default AuctionAssignmentMap;
@@ -0,0 +1,14 @@
.auction-collateral-selection
{
margin-top: 0.5em;
}
.auction-collateral-selection
.card-body:not(.bg-warning) {
cursor: pointer;
}
.auction-collateral-selection
.bg-warning {
cursor: default;
}
@@ -0,0 +1,131 @@
import React from 'react';
import { shallow } from 'enzyme';
import AuctionCollateralSelection, { ICollateralSelection, IAuctionCollateralSelectionProps } from './AuctionCollateralSelection';
describe('Auction Collateral Selection Component', () => {
let auctionCollateralSelection: AuctionCollateralSelection;
let originalCollateralSelections: ICollateralSelection[];
let defaultProps: IAuctionCollateralSelectionProps;
let updatedProps:IAuctionCollateralSelectionProps;
beforeEach(() => {
originalCollateralSelections = [
{
collateral: {
collateralId: 1,
repoId: 1,
vin: '1234',
make: 'ford',
model: 'f150',
year: 1996,
repoCompanySelections: []
},
isSelected: false,
},
{
collateral: {
collateralId: 2,
repoId: 1,
vin: 'abcd',
make: 'dodge',
model: 'ram',
year: 2001,
auctionAssignmentId: 1,
repoCompanySelections: []
},
isSelected: false,
},
];
defaultProps = {
collateralSelections: originalCollateralSelections,
vehicleTypes: [],
collateralTypes: [],
bodyStyles: [],
collateralTypesToVehicleTypes: [],
onCollateralSelectionsChanged: (cs) => updatedProps.collateralSelections = cs,
onPrimaryCollateralChanged: (cid) => updatedProps.primaryCollateralId = cid,
};
updatedProps = Object.assign({}, defaultProps);
auctionCollateralSelection = new AuctionCollateralSelection(defaultProps);
});
it ('Renders AuctionCollateralSelectionRows', () => {
let wrapper = shallow(
<AuctionCollateralSelection
collateralSelections={originalCollateralSelections}
vehicleTypes={[]}
collateralTypes={[]}
bodyStyles={[]}
collateralTypesToVehicleTypes={[]}
onCollateralSelectionsChanged={() => {}}
onPrimaryCollateralChanged={() => {}}
/>
);
let selectionRow = wrapper.find(`[data-test='auction-collateral-selection-row']`);
expect(selectionRow.length).toBe(2);
});
it ('Updates Primary Collateral Id', () => {
auctionCollateralSelection.setPrimaryCollateral(originalCollateralSelections[0]);
expect(updatedProps.primaryCollateralId).toBe(1);
auctionCollateralSelection.setPrimaryCollateral(originalCollateralSelections[1]);
expect(updatedProps.primaryCollateralId).toBe(2);
});
it ('Selecting Collateral Copies and Calls onCollateralSelectionsChanged()', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.collateralSelections).not.toBeUndefined();
expect(updatedProps.collateralSelections).not.toBe(originalCollateralSelections);
expect(updatedProps.collateralSelections[0].isSelected).toBe(true);
});
it ('Selecting Collateral Without Primary Collateral Calls onPrimaryCollateralChanged()', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(1);
});
it ('Selecting Multiple Collaterals Does Not Continue Calling onPrimaryCollateralChanged()', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(1);
auctionCollateralSelection.toggleCollateralByIndex(1);
expect(updatedProps.primaryCollateralId).toBe(1);
});
it ('Unselecting All Collaterals Calls onPrimaryCollateralChanged() With Undefined', () => {
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(1);
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBeUndefined();
});
it ('Unselecting Primary Collateral With Another Selection Calls onPrimaryCollateralChanged() With Valid Id', () => {
updatedProps.collateralSelections[0].isSelected = true;
updatedProps.collateralSelections[1].isSelected = true;
updatedProps.primaryCollateralId = 1;
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(updatedProps.primaryCollateralId).toBe(2);
});
it ('Selecting Collateral With Auction Triggers Nothing', () => {
let wasSelectionsUpdated = false;
let wasPrimaryCollateralUpdated = false;
updatedProps.collateralSelections[0].collateral.auctionAssignmentId = 1;
updatedProps.onCollateralSelectionsChanged = () => wasSelectionsUpdated = true;
updatedProps.onPrimaryCollateralChanged = () => wasPrimaryCollateralUpdated = true;
auctionCollateralSelection = new AuctionCollateralSelection(updatedProps);
auctionCollateralSelection.toggleCollateralByIndex(0);
expect(wasSelectionsUpdated).toBe(false);
expect(wasPrimaryCollateralUpdated).toBe(false);
});
});
@@ -0,0 +1,110 @@
import React from 'react';
import { IVehicleType, ICollateralType, IBodyStyle, ICollateralTypesToVehicleType } from "../../reducers/SettingsReducer";
import './AuctionCollateralSelection.css';
import AuctionCollateralSelectionRow from './auction-collateral-selection-row/AuctionCollateralSelectionRow';
import { ICollateral } from '../../services/service-types/RepoAccountTypes';
const cloneDeep = require('lodash.clonedeep');
export interface ICollateralSelection {
collateral: ICollateral;
isSelected: boolean;
}
export interface IAuctionCollateralSelectionProps {
collateralSelections: ICollateralSelection[];
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
primaryCollateralId?: number;
onCollateralSelectionsChanged: (collateralSelections: ICollateralSelection[]) => any;
onPrimaryCollateralChanged: (collateralId?: number) => any;
}
class AuctionCollateralSelection extends React.Component<IAuctionCollateralSelectionProps> {
constructor(props: IAuctionCollateralSelectionProps) {
super(props);
this.isCollateralSelectionPrimary = this.isCollateralSelectionPrimary.bind(this);
}
applyUpdate (index: number, updater: (x: ICollateralSelection) => void) {
const copiedCollaterSpecSelection = cloneDeep(this.props.collateralSelections) as ICollateralSelection[];
updater(copiedCollaterSpecSelection[index]);
this.props.onCollateralSelectionsChanged(copiedCollaterSpecSelection);
};
collateralUpdated(index: number, collateral: ICollateral) {
this.applyUpdate(index, (collateralSelection) => {
collateralSelection.collateral = collateral
});
};
toggleCollateralByIndex(index: number) {
const collateralSelection = this.props.collateralSelections[index];
if (this.isCollateralSelectable(collateralSelection)) {
if (!this.props.primaryCollateralId) {
// There is no primary collateral selected, default to this one
this.props.onPrimaryCollateralChanged(collateralSelection.collateral.collateralId);
} else if (collateralSelection.isSelected && this.isCollateralSelectionPrimary(collateralSelection)) {
// The selection with the primary collateral is about to be closed.
// Try and find a available collateral to be primary
this.props.onPrimaryCollateralChanged(this.getNextPrimaryCollateralId());
}
this.applyUpdate(index, (collateralSelection) => {
collateralSelection.isSelected = !collateralSelection.isSelected;
});
}
};
getNextPrimaryCollateralId() {
const nextCollateralSelection = this.props.collateralSelections.find(
(collateralSelection) => collateralSelection.isSelected && !this.isCollateralSelectionPrimary(collateralSelection)
);
if (nextCollateralSelection) {
return nextCollateralSelection.collateral.collateralId;
}
}
setPrimaryCollateral(collateralSelection: ICollateralSelection) {
this.props.onPrimaryCollateralChanged(
collateralSelection.collateral.collateralId,
)
}
isCollateralSelectable(collateralSelection: ICollateralSelection): boolean {
return !collateralSelection.collateral.auctionAssignmentId;
}
isCollateralSelectionPrimary(collateralSelection: ICollateralSelection) {
return this.props.primaryCollateralId === collateralSelection.collateral.collateralId;
}
render() {
return (
<div className="card auction-collateral-selection">
{this.props.collateralSelections.map((collateralSelection, index) =>
<AuctionCollateralSelectionRow
key={collateralSelection.collateral.collateralId}
collateralSelection={collateralSelection}
vehicleTypes={this.props.vehicleTypes}
collateralTypes={this.props.collateralTypes}
bodyStyles={this.props.bodyStyles}
collateralTypesToVehicleTypes={this.props.collateralTypesToVehicleTypes}
isCollateralPrimary={this.isCollateralSelectionPrimary(collateralSelection)}
onCollateralChange={(collateral) => this.collateralUpdated(index, collateral)}
onIsPrimaryClicked={() => this.setPrimaryCollateral(collateralSelection)}
onRowClicked={() => this.toggleCollateralByIndex(index)}
data-test="auction-collateral-selection-row"
/>
)}
</div>
)
}
};
export default AuctionCollateralSelection;
@@ -0,0 +1,88 @@
import React from 'react';
import { ShallowWrapper, shallow } from 'enzyme';
import AuctionCollateralSelectionRow from './AuctionCollateralSelectionRow';
import { ICollateralSelection } from '../AuctionCollateralSelection';
import moment from 'moment';
describe('AuctionCollateralSelectionRow Component', () => {
let wrapper: ShallowWrapper;
let collateralSelection: ICollateralSelection;
beforeEach(() => {
collateralSelection = {
isSelected: false,
collateral: {
collateralId: 2,
repoId: 1,
vin: 'abcd',
make: 'dodge',
model: 'ram',
year: 2001,
repoCompanySelections: []
},
};
wrapper = shallow(
<AuctionCollateralSelectionRow
collateralSelection={collateralSelection}
vehicleTypes={[]}
collateralTypes={[]}
bodyStyles={[]}
collateralTypesToVehicleTypes={[]}
isCollateralPrimary={false}
onCollateralChange={() => {}}
onIsPrimaryClicked={() => {}}
onRowClicked={() => {}}
/>
);
});
it ('Displays Collateral Correctly', () => {
const collateralDescription = wrapper.find(`[data-test='auction-collateral-selection-collateral-description']`);
expect(collateralDescription.length).toBe(1);
expect(collateralDescription.text()).toEqual('2001 dodge ram');
});
it ('Displays Collateral Selection Statuses Correctly', () => {
const collateralDescription = wrapper.find(`[data-test='auction-collateral-selection-collateral-description']`);
expect(collateralDescription.length).toBe(1);
expect(collateralDescription.text()).toEqual('2001 dodge ram');
let selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
expect(selectionStatus.length).toBe(1);
expect(selectionStatus.text().trim()).toEqual('(Not Selected)');
wrapper.setProps({
collateralSelection: {
...collateralSelection,
isSelected: true,
},
});
selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
expect(selectionStatus.length).toBe(1);
expect(selectionStatus.text().trim()).toEqual('(Selected)');
// wrapper.setProps({
// collateralSelection: {
// collateral: {
// ...collateralSelection.collateral,
// auctionAssignmentId: 1,
// },
// isSelected: false,
// },
// });
// selectionStatus = wrapper.find(`[data-test='auction-collateral-selection-header-status-text']`);
// expect(selectionStatus.length).toBe(1);
// expect(selectionStatus.text().trim()).toEqual('(Auction Already Created)');
});
it ('Calls Callback on Click', () => {
let wasCallTriggered = false;
wrapper.setProps({
onRowClicked: () => wasCallTriggered = true,
});
let rowHeader = wrapper.find(`[data-test='auction-collateral-selection-header']`);
rowHeader.simulate('click');
expect(wasCallTriggered).toBe(true);
});
});
@@ -0,0 +1,96 @@
import React from "react";
import { IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
import AuctionCollateralSpecs from "../auction-collateral-specs/AuctionCollateralSpecs";
import { ICollateralSelection } from "../AuctionCollateralSelection";
interface IAuctionCollateralSelectionRowProps {
collateralSelection: ICollateralSelection;
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
isCollateralPrimary: boolean;
onCollateralChange: (collateral: ICollateral) => any;
onIsPrimaryClicked: () => any;
onRowClicked: () => any;
}
const AuctionCollateralSelectionRow: React.FC<IAuctionCollateralSelectionRowProps> = (props) => {
const getCollateralDisplayName = () => {
const collateral = props.collateralSelection.collateral;
return `${collateral.year} ${collateral.make} ${collateral.model}`;
};
const getHeaderStatusText = () => {
if (props.collateralSelection.isSelected) {
return " (Selected)";
}
return " (Not Selected)";
};
const getHeaderBGClass = () => {
if (props.collateralSelection.isSelected) {
return "bg-success";
}
return "bg-light";
};
const getHeaderColorClass = () => {
if (props.collateralSelection.isSelected) {
return "text-white";
}
return "text-dark";
};
const getFontAwesomeIconClass = () => {
if (props.collateralSelection.isSelected) {
return "fa-check-square-o";
}
return "fa-square-o";
};
return (
<div className="auction-collateral-selection-row">
<div
className={"card-body " + getHeaderBGClass()}
onClick={props.onRowClicked}
data-test="auction-collateral-selection-header"
>
<h6 className={getHeaderColorClass()}>
<i className={"fa " + getFontAwesomeIconClass()} aria-hidden="true"/>
&nbsp;
<span data-test="auction-collateral-selection-collateral-description">
{getCollateralDisplayName()}
</span>
<i data-test="auction-collateral-selection-header-status-text">
{getHeaderStatusText()}
</i>
</h6>
</div>
{
props.collateralSelection.isSelected
?
<ul className="list-group list-group-flush" data-test="auction-collateral-selection-opened-section">
<li className="list-group-item">
<AuctionCollateralSpecs
collateral={props.collateralSelection.collateral}
vehicleTypes={props.vehicleTypes}
collateralTypes={props.collateralTypes}
bodyStyles={props.bodyStyles}
collateralTypesToVehicleTypes={props.collateralTypesToVehicleTypes}
onCollateralChange={props.onCollateralChange}
isCollateralPrimary={props.isCollateralPrimary}
onIsPrimaryClicked={props.onIsPrimaryClicked}
/>
</li>
</ul>
: null
}
</div>
);
};
export default AuctionCollateralSelectionRow;
@@ -0,0 +1,4 @@
.is-collateral-primary-check,
.is-collateral-primary-check * {
cursor: pointer;
}
@@ -0,0 +1,217 @@
import { shallow, ShallowWrapper } from "enzyme";
import React from "react";
import { IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
import AuctionCollateralSpecs from "./AuctionCollateralSpecs";
describe("Auction Collateral Specs Component", () => {
let wrapper: ShallowWrapper;
let collateralTypes: ICollateralType[];
let collateralTypeToVehicleTypes: ICollateralTypesToVehicleType[];
let bodyStyles: IBodyStyle[];
let vehicleTypes: IVehicleType[];
let originalCollateral: ICollateral;
let updatedCollateral: ICollateral | undefined;
const collateralUpdated = (newCollateral: ICollateral) => {
updatedCollateral = newCollateral;
};
beforeEach(() => {
updatedCollateral = undefined;
originalCollateral = {
collateralId: 1,
repoId: 1,
vin: "1234",
make: "ford",
model: "f150",
year: 1996,
repoCompanySelections: []
};
collateralTypes = [
{
id: 1,
description: "Collateral Type 1",
lookupHandler: "",
},
{
id: 2,
description: "Collateral Type 2",
lookupHandler: "",
},
{
id: 3,
description: "Collateral Type 3",
lookupHandler: "",
},
];
collateralTypeToVehicleTypes = [
{
id: 1,
vehicleTypeId: 1,
collateralTypeId: 1,
},
{
id: 2,
vehicleTypeId: 1,
collateralTypeId: 2,
},
{
id: 3,
vehicleTypeId: 2,
collateralTypeId: 3,
},
];
bodyStyles = [
{
code: "body1",
description: "Body Style #1",
},
{
code: "body2",
description: "Body Style #2",
},
];
vehicleTypes = [
{
code: "1",
description: "Vehicle Type #1",
},
{
code: "2",
description: "Vehicle Type #2",
},
{
code: "3",
description: "Vehicle Type #3",
},
];
wrapper = shallow(
<AuctionCollateralSpecs
collateral={originalCollateral}
vehicleTypes={vehicleTypes}
collateralTypes={collateralTypes}
bodyStyles={bodyStyles}
collateralTypesToVehicleTypes={collateralTypeToVehicleTypes}
onCollateralChange={collateralUpdated}
isCollateralPrimary={false}
onIsPrimaryClicked={() => {}}
/>
);
});
it("Renders & Updates Vehicle Type", () => {
// Inital value is present
let vehicleTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-vehicle-type']`);
expect(vehicleTypeSelect.length).toBe(1);
expect(vehicleTypeSelect.props().value).toBe(undefined);
// Change triggers callback
vehicleTypeSelect.simulate("change", "1");
expect(updatedCollateral).not.toBeUndefined();
expect(updatedCollateral!.vehicleType).toEqual(parseInt(vehicleTypes[0].code));
// Change does not alter original prop object
expect(originalCollateral).not.toBe(updatedCollateral);
// Prop changes update input
wrapper = wrapper.setProps({
collateral: {
vehicleType: 2,
},
});
vehicleTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-vehicle-type']`);
expect(vehicleTypeSelect.props().value).toBe(2);
});
it("Renders & Updates Body Style", () => {
// Inital value is present
let bodyStyleSelect = wrapper.find(`[data-test='auction-collateral-specs-body-style']`);
expect(bodyStyleSelect.length).toBe(1);
expect(bodyStyleSelect.props().value).toBe(undefined);
// Change triggers callback
bodyStyleSelect.simulate("change", "body1");
expect(updatedCollateral).not.toBeUndefined();
expect(updatedCollateral!.bodyStyle).toEqual(bodyStyles[0].code);
// Change does not alter original prop object
expect(originalCollateral).not.toBe(updatedCollateral);
// Prop changes update input
wrapper = wrapper.setProps({
collateral: {
bodyStyle: "body2",
},
});
bodyStyleSelect = wrapper.find(`[data-test='auction-collateral-specs-body-style']`);
expect(bodyStyleSelect.props().value).toBe("body2");
});
it("Renders & Updates Collateral Style", () => {
// Inital value is present
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
// Change triggers callback
collateralTypeSelect.simulate("change", "3");
expect(updatedCollateral).not.toBeUndefined();
expect(updatedCollateral!.collateralTypeId).toEqual(collateralTypes[2].id);
// Change does not alter original prop object
expect(originalCollateral).not.toBe(updatedCollateral);
// Prop changes update input
wrapper = wrapper.setProps({
collateral: {
collateralTypeId: 2
},
});
collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.props().value).toBe(2);
});
it("Disabled Collateral Type Select when Vehicle Type is Undefined", () => {
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
expect(collateralTypeSelect.props().disabled).toBe(true);
});
it("No Collateral Type Options when Vehicle Type Does Not Match Collateral Types", () => {
wrapper = wrapper.setProps({
collateral: {
vehicleType: 3,
},
});
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
expect(collateralTypeSelect.props().disabled).toBe(false);
});
it("Valid Collateral Types Options when Vehicle Type Matches Collateral Types", () => {
wrapper = wrapper.setProps({
collateral: {
vehicleType: 1,
},
});
let collateralTypeSelect = wrapper.find(`[data-test='auction-collateral-specs-collateral-type']`);
expect(collateralTypeSelect.length).toBe(1);
expect(collateralTypeSelect.props().value).toBe(undefined);
expect(collateralTypeSelect.props().disabled).toBe(false);
});
it ("Calls onIsPrimaryClicked() When Checkbox is Clicked", () => {
let wasCallTriggered = false;
wrapper.setProps({
onIsPrimaryClicked: () => wasCallTriggered = true,
});
let isPrimaryContainer = wrapper.find(`[data-test='auction-collateral-specs-is-primary-container']`);
isPrimaryContainer.simulate("click");
expect(wasCallTriggered).toBe(true);
});
});
@@ -0,0 +1,146 @@
import React from "react";
import { getCollateralTypesFromVehicleType, IBodyStyle, ICollateralType, ICollateralTypesToVehicleType, IVehicleType } from "../../../reducers/SettingsReducer";
import { ICollateral } from "../../../services/service-types/RepoAccountTypes";
import FormInput, { FormInputType } from "../../form-input/FormInput";
import "./AuctionCollateralSpecs.css";
const cloneDeep = require("lodash.clonedeep");
export interface IAuctionCollateralSpecsProps {
collateral: ICollateral;
vehicleTypes: IVehicleType[];
collateralTypes: ICollateralType[];
bodyStyles: IBodyStyle[];
collateralTypesToVehicleTypes: ICollateralTypesToVehicleType[];
isCollateralPrimary: boolean;
onCollateralChange: (collateral: ICollateral) => any;
onIsPrimaryClicked: () => any;
}
type ICollateralUpdater = (collateral: ICollateral) => void;
const AuctionCollateralSpecs: React.FC<IAuctionCollateralSpecsProps> = (props) => {
const applyChange = function( updater: ICollateralUpdater) {
const copiedCollateral = cloneDeep(props.collateral) as ICollateral;
updater(copiedCollateral);
props.onCollateralChange(copiedCollateral);
};
const handlePrimaryCollateralClicked = () => {
props.onIsPrimaryClicked();
};
const getCollateralDescription = () => {
return `${props.collateral.year} ${props.collateral.make} ${props.collateral.model} ${props.collateral.vin}`;
};
const getCollateralTypes = (vehicleTypeCode?: number) => {
return getCollateralTypesFromVehicleType(
props.collateralTypesToVehicleTypes,
props.collateralTypes,
vehicleTypeCode
);
};
const handleVehicleTypeChange = (selection: string) => {
applyChange((collateral) => {
collateral.vehicleType = selection ? parseInt(selection) : undefined;
const matchedCollateralTypes = getCollateralTypes(collateral.vehicleType);
if (matchedCollateralTypes.length === 1) {
collateral.collateralTypeId = matchedCollateralTypes[0].id;
} else {
collateral.collateralTypeId = undefined;
}
});
};
const isVehicleTypeSelected = () => {
return props.collateral.vehicleType !== undefined && props.collateral.vehicleType !== null;
};
const currentlyMatchedCollateralTypes = getCollateralTypesFromVehicleType(
props.collateralTypesToVehicleTypes,
props.collateralTypes,
props.collateral.vehicleType
);
return (
<div className="container">
<div className="row">
<div className="col-sm">
<div
className="custom-control custom-checkbox is-collateral-primary-check"
onClick={handlePrimaryCollateralClicked}
data-test="auction-collateral-specs-is-primary-container"
>
<input
type="checkbox"
className="custom-control-input"
checked={props.isCollateralPrimary}
readOnly={true}
/>
<label className="custom-control-label">Is Primary</label>
</div>
</div>
<div className="col-sm">
{getCollateralDescription()}
</div>
<div className="col-sm">
<FormInput
label="Vehicle Type"
required={true}
type={FormInputType.Selection}
value={props.collateral.vehicleType}
onChange={(vehicleType) => handleVehicleTypeChange(vehicleType)}
placeholder="Select Vehicle Type"
options={props.vehicleTypes.map((vehicleType) => {
return {
value: vehicleType.code,
display: vehicleType.description
};
})}
data-test="auction-collateral-specs-vehicle-type"
/>
</div>
<div className="col-sm">
<FormInput
label="Body Style"
required={true}
type={FormInputType.Selection}
value={props.collateral.bodyStyle}
onChange={(bodyStyle) => applyChange((collateral) => collateral.bodyStyle = bodyStyle)}
placeholder="Select Body Style"
options={props.bodyStyles.map((bodyStyle) => {
return {
value: bodyStyle.code,
display: bodyStyle.description
};
})}
data-test="auction-collateral-specs-body-style"
/>
</div>
<div className="col-sm">
<FormInput
label="Collateral Type"
required={true}
type={FormInputType.Selection}
value={props.collateral.collateralTypeId}
onChange={(collateralType) => applyChange((collateral) => collateral.collateralTypeId = parseInt(collateralType))}
placeholder="Select Collateral Type"
disabled={!isVehicleTypeSelected()}
options={currentlyMatchedCollateralTypes.map((collateralType) => {
return {
value: collateralType.id,
display: collateralType.description
};
})}
data-test="auction-collateral-specs-collateral-type"
/>
</div>
</div>
</div>
);
};
export default AuctionCollateralSpecs;
@@ -0,0 +1,16 @@
.card-view-main {
margin-top: 1em;
margin-bottom: 1em;
}
.card-view-main .card-body {
padding: .25em;
}
.card-view-main h6 {
margin: .5em;
}
.card-view-main .list-group-item {
padding: .25em 1em;
}
@@ -0,0 +1,46 @@
import React from 'react';
import { shallow } from "enzyme";
import CardView from './CardView';
describe('CardView Component', () => {
it('CardView Displays No Header', () => {
const cardView = shallow(<CardView rows={[]} />);
const header = cardView.find(`[data-test='card-view-header']`);
expect(header.length).toBe(0);
});
it('CardView Displays No Rows', () => {
const cardView = shallow(<CardView rows={[]} />);
const rows = cardView.find(`[data-test='card-view-row-parent']`);
expect(rows.length).toBe(0);
});
it('CardView Displays Header', () => {
const cardView = shallow(<CardView header="Header Test" rows={[]} />);
const header = cardView.find(`[data-test='card-view-header']`);
expect(header.length).toBe(1);
expect(header.text()).toEqual('Header Test');
});
it('CardView Displays Valid Text Rows', () => {
const cardView = shallow(<CardView rows={[
'Row 1',
'Row 2',
]} />);
const rows = cardView.find(`[data-test='card-view-row-parent']`);
expect(rows.length).toBe(2);
expect(rows.at(0).text()).toEqual('Row 1');
expect(rows.at(1).text()).toEqual('Row 2');
});
it('CardView Displays Valid HTML Rows', () => {
const cardView = shallow(<CardView rows={[
<p data-test='card-view-test-html-child'>Child Text</p>,
]} />);
const pChild = cardView.find(`[data-test='card-view-test-html-child']`);
expect(pChild.length).toBe(1);
expect(pChild.text()).toEqual('Child Text');
});
});
@@ -0,0 +1,39 @@
import React from 'react';
import './CardView.css';
interface ICardViewProps {
header?: any;
rows: any[];
}
const CardView: React.FC<ICardViewProps> = (props) => {
const tryDisplayHeader = () => {
if (props.header) {
return (
<div className="card-body bg-secondary">
<h6 className="text-white" data-test="card-view-header">{props.header}</h6>
</div>
)
}
}
return (
<div className="card card-view-main">
{tryDisplayHeader()}
<ul className="list-group list-group-flush">
{
props.rows.map((item, index) => {
return (
<li key={index} className="list-group-item" data-test="card-view-row-parent">
{item}
</li>
)
})
}
</ul>
</div>
)
}
export default CardView;
@@ -0,0 +1,3 @@
.collateral-view-row p {
margin-bottom: 0;
}
@@ -0,0 +1,116 @@
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import CollateralDisplay, { ICollateralDisplay } from './CollateralDisplay';
import moment from 'moment';
import { ICollateral } from '../../services/service-types/RepoAccountTypes';
describe('CollateralDisplay Component', () => {
let collateralDisplay: ShallowWrapper;
let retitleClickValue = 0;
let refreshCollateralValue = false;
const retitledClicked = (collateralId: number) => {
retitleClickValue = collateralId;
};
const refreshCollateralClicked = () => {
refreshCollateralValue = true;
}
let dummyCollateral1: ICollateralDisplay = {
collateral: {
vin: '1234abcd',
make: 'ford',
model: 'fiesta',
year: 1865,
collateralId: 1970,
} as ICollateral,
refreshCollateral: refreshCollateralClicked,
}
let dummyCollateral2: ICollateralDisplay = {
collateral: {
vin: 'abcd1234',
make: 'ferrari',
model: 'enzo',
year: 1916,
collateralId: 1968,
refCollateralId: '1998',
retitledInLandmarkName: moment('1914-06-28'),
} as ICollateral,
refreshCollateral: () => {},
}
beforeEach(() => {
collateralDisplay = shallow(
<CollateralDisplay
isAccountClosed={false}
collateral={[dummyCollateral1, dummyCollateral2]}
/>
)
});
afterEach(() => {
retitleClickValue = 0;
refreshCollateralValue = false;
})
const getChildCardView = () => {
return collateralDisplay.find('CardView').dive();
}
it('CollateralDisplay Renders Year', () => {
const collateralYear = getChildCardView().find(`[data-test='collateral-display-year']`);
expect(collateralYear.length).toBe(2);
expect(collateralYear.at(0).text()).toEqual('1865');
expect(collateralYear.at(1).text()).toEqual('1916');
});
it('CollateralDisplay Renders Make', () => {
const collateralMake = getChildCardView().find(`[data-test='collateral-display-make']`);
expect(collateralMake.length).toBe(2);
expect(collateralMake.at(0).text()).toEqual('ford');
expect(collateralMake.at(1).text()).toEqual('ferrari');
});
it('CollateralDisplay Renders Model', () => {
const collateralModel = getChildCardView().find(`[data-test='collateral-display-model']`);
expect(collateralModel.length).toBe(2);
expect(collateralModel.at(0).text()).toEqual('fiesta');
expect(collateralModel.at(1).text()).toEqual('enzo');
});
it('CollateralDisplay Renders Vin', () => {
const collateralVin = getChildCardView().find(`[data-test='collateral-display-vin']`);
expect(collateralVin.length).toBe(2);
expect(collateralVin.at(0).text()).toEqual('1234abcd');
expect(collateralVin.at(1).text()).toEqual('abcd1234');
});
it('CollateralDisplay Collateral Refresh Button is Disabled', () => {
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
expect(collateralRefreshBtn.length).toBe(2);
expect(collateralRefreshBtn.at(0).props().disabled).toEqual(true);
});
it('CollateralDisplay Collateral Refresh Button is Enabled', () => {
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
expect(collateralRefreshBtn.length).toBe(2);
expect(collateralRefreshBtn.at(1).props().disabled).toEqual(false);
});
it('CollateralDisplay Calls Collateral Refresh Callback', () => {
const collateralRefreshBtn = getChildCardView().find(`[data-test='collateral-display-refresh-btn']`);
expect(collateralRefreshBtn.length).toBe(2);
collateralRefreshBtn.at(0).simulate('click');
expect(refreshCollateralValue).toEqual(true);
});
it('CollateralDisplay Displays Retitled Date', () => {
const collateralRetitleDate = getChildCardView().find(`[data-test='collateral-display-retitle-date']`);
expect(collateralRetitleDate.length).toBe(2);
expect(collateralRetitleDate.last().text()).toBe('1914-06-28');
expect(collateralRetitleDate.first().text()).toBe('');
});
});
@@ -0,0 +1,59 @@
import React from "react";
import { ICollateral } from "../../services/service-types/RepoAccountTypes";
import CardView from "../card-view/CardView";
import DateFormatting from "./../../shared/formatting/DateFormatting";
import "./CollateralDisplay.css";
export interface ICollateralDisplay {
collateral: ICollateral;
refreshCollateral: () => any;
}
export interface ICollateralDisplayProps {
isAccountClosed: boolean;
collateral: ICollateralDisplay[];
}
const CollateralDisplay: React.FC<ICollateralDisplayProps> = (props: ICollateralDisplayProps) => {
return (
<CardView
rows={props.collateral.map((collateralDisplay) => (
<div key={collateralDisplay.collateral.collateralId} className="row collateral-view-row">
<div className="col-sm">
<p>Year:</p>
<p><b data-test="collateral-display-year">{collateralDisplay.collateral.year}</b></p>
</div>
<div className="col-sm">
<p>Make:</p>
<p><b data-test="collateral-display-make">{collateralDisplay.collateral.make}</b></p>
</div>
<div className="col-sm">
<p>Model:</p>
<p><b data-test="collateral-display-model">{collateralDisplay.collateral.model}</b></p>
</div>
<div className="col-sm">
<p>VIN:</p>
<p><b data-test="collateral-display-vin">{collateralDisplay.collateral.vin}</b></p>
</div>
<div className="col-sm">
<p>Retitled Date:</p>
<p><b data-test="collateral-display-retitle-date">{DateFormatting.FormatYYYYMMDD(collateralDisplay.collateral.retitledInLandmarkName)}</b></p>
</div>
<div className="col-sm text-center my-auto">
<button
className={"btn " + (props.isAccountClosed ? "btn-secondary" : "btn-info")}
onClick={() => collateralDisplay.refreshCollateral()}
disabled={!collateralDisplay.collateral.refCollateralId || props.isAccountClosed}
data-test="collateral-display-refresh-btn"
>
Collateral Refresh
</button>
</div>
</div>
))}
/>
);
};
export default CollateralDisplay;
@@ -0,0 +1,3 @@
.vin-disp {
font-size: smaller;
}
@@ -0,0 +1,31 @@
import React from 'react';
import {shallow, ShallowWrapper} from 'enzyme';
import CollateralTextSummary, {ICollateralSummaryInfo} from "./CollateralTextSummary";
describe('CollateralTextSummary Component', () => {
let collateralDisplay: ShallowWrapper;
let dummyCollateral1: ICollateralSummaryInfo = {
vin: '1234abcd',
make: 'ford',
model: 'FIESTA',
year: 1865,
collateralId: 1970
};
beforeEach(() => {
collateralDisplay = shallow(
<CollateralTextSummary collateral={dummyCollateral1}/>
)
});
const getView = () => {
return collateralDisplay.find('span');
};
it('CollateralTextSummary Renders ', () => {
const collateral = getView();
expect(collateral.first().text()).toEqual('1865 Ford FIESTA 1234abcd');
});
});
@@ -0,0 +1,23 @@
import React from 'react';
import "./CollateralTextSummary.css";
import StringFormatting from "../../../shared/formatting/StringFormatting";
export interface ICollateralSummaryInfo {
vin: string;
make: string;
model: string;
year: number;
collateralId: number;
}
export interface ICollateralTextSummaryProps {
collateral: ICollateralSummaryInfo;
}
const CollateralTextSummary: React.FC<ICollateralTextSummaryProps> = (props: ICollateralTextSummaryProps) => {
return (
<span>{props.collateral.year} {StringFormatting.ToTitleCase(props.collateral.make)} {props.collateral.model} <span className='vin-disp'>{props.collateral.vin}</span></span>
);
};
export default CollateralTextSummary;
@@ -0,0 +1,69 @@
import React from 'react';
import {shallow} from "enzyme";
import {_AuctionDisplay} from "./AuctionDisplay";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAuctionHouse} from "../../reducers/AuctionHouseReducer";
import AsyncState from "../../shared/enumerations/AsyncState";
describe('AuctionDisplay Component', () => {
it('Auction looks up company by id', () => {
const loader = () => {};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [],
state: AsyncState.Success
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(aucDisplay.length).toBe(1);
expect(aucDisplay.text()).toBe("mmaa");
});
it('Load if status is not success', () => {
const loader = () => {};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [],
state: AsyncState.Loading
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(aucDisplay.length).toBe(1);
expect(aucDisplay.text()).toBe("Loading...")
});
it('Loads Auction Houses When Inactive', () => {
let didLoad = false;
const loader = () => {didLoad = !didLoad};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [],
state: AsyncState.Inactive
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(didLoad).toBe(true);
});
it("Display Auction House Name when Auction House Name Found", () => {
const loader = () => {};
const state:IAsyncStateContainer<IAuctionHouse[]> = {
data: [{auctionID: "mmaa",
name: "name",
active: "yes",
address_Line_1: "11",
address_Line_2: "11",
state: "WI",
city: "Eagle Creek",
phone: "13131",
fax: "123213",
zip: 55555,
country: "USA" }
],
state: AsyncState.Success
};
const component = shallow(<_AuctionDisplay auctionHouses={state} loadAuctionHouses={loader} auctionId="mmaa" />);
const aucDisplay = component.find(`[data-test='mmaa-auc-display']`);
expect(aucDisplay.text()).toBe("name");
});
});
@@ -0,0 +1,60 @@
import React from "react";
import {connect} from "react-redux";
import {getAllAuctionHouses} from "../../actions/AuctionHouseActions";
import {IAuctionHouse} from "../../reducers/AuctionHouseReducer";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../store/Store";
interface IAuctionDisplayProps {
auctionId: string;
}
interface IPropsFromState {
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
auctionHouses: state.auctionHouseState.auctionHouses,
};
};
interface IPropsFromDispatch {
loadAuctionHouses: () => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
loadAuctionHouses: getAllAuctionHouses,
};
type Props = IAuctionDisplayProps & IPropsFromState & IPropsFromDispatch;
const AuctionDisplay: React.FC<Props> = (props) => {
const auctionLookup = (auctionId: string): string => {
// Did not put this in useEffect because it has an odd timing issue and they ALL load.
if (props.auctionHouses.state === AsyncState.Inactive) {
props.loadAuctionHouses();
}
if (props.auctionHouses && props.auctionHouses.state === AsyncState.Success) {;
const auctionHouseSet = props.auctionHouses.data as IAuctionHouse[];
const auctionHouse = auctionHouseSet.find((house: IAuctionHouse) => {
return house.auctionID.toLowerCase() === props.auctionId.toLowerCase();
});
return auctionHouse ? auctionHouse.name : props.auctionId;
} else {
return "Loading..."; // @TODO switch to showing notification.
}
};
return (
<span data-test={props.auctionId + "-auc-display"}>{auctionLookup(props.auctionId)}</span>
);
};
export const _AuctionDisplay = AuctionDisplay;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AuctionDisplay);
@@ -0,0 +1,67 @@
import React from 'react';
import {shallow} from "enzyme";
import {_RepoCompanyDisplay} from "./RepoCompanyDisplay";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import AsyncState from "../../shared/enumerations/AsyncState";
import {IRepoCompany} from "../../reducers/RepoCompanyReducer";
const findByTestAttribute = (component: ShallowWrapper, attr: string) => {
const wrapper = component.find(`[data-test='${attr}']`);
return wrapper;
};
describe('Repo Company Display Component', () => {
it('looks up company by id', () => {
const loader = () => {};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [],
state: AsyncState.Success
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(aucDisplay.length).toBe(1);
});
it('displays loading if status is not success', () => {
const loader = () => {};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [],
state: AsyncState.Loading
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(aucDisplay.length).toBe(1);
expect(aucDisplay.text()).toBe("Loading...")
});
it('Calls Load repo Companies when inactive',() => {
var didLoad = false;
const loader = () => {didLoad = !didLoad};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [],
state: AsyncState.Inactive
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(didLoad).toBe(true);
});
it('Displays Repo Company Name when Repo Company Name Is Found', () => {
const loader = () => {};
const state:IAsyncStateContainer<IRepoCompany[]> = {
data: [
{id: 23,
name: "company"
}
],
state: AsyncState.Success
};
const component = shallow(<_RepoCompanyDisplay repoCompanies={state} loadRepoCompanies={loader} repoCompanyId={23} />);
const aucDisplay = component.find(`[data-test='23-rc-display']`);
expect(aucDisplay.text()).toBe("company")
});
});
@@ -0,0 +1,60 @@
import React from 'react';
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../store/Store";
import AsyncState from "../../shared/enumerations/AsyncState";
import {connect} from "react-redux";
import {IRepoCompany} from "../../reducers/RepoCompanyReducer";
import {getAllRepoCompanies} from "../../actions/RepoCompanyActions";
interface IRepoCompanyDisplayProps {
repoCompanyId: number;
}
interface IPropsFromState {
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoCompanies: state.repoCompanyState.repoCompanies,
};
};
interface IPropsFromDispatch {
loadRepoCompanies: () => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
loadRepoCompanies: getAllRepoCompanies,
};
type Props = IRepoCompanyDisplayProps & IPropsFromState & IPropsFromDispatch;
const RepoCompanyDisplay: React.FC<Props> = (props) => {
const RepoCompanyLookup = (repoCompanyId: number): string => {
// Did not put this in useEffect because it has an odd timing issue and they ALL load.
if (props.repoCompanies.state === AsyncState.Inactive) {
props.loadRepoCompanies();
}
if (props.repoCompanies && props.repoCompanies.state === AsyncState.Success) {
const repoCompanieSet = props.repoCompanies.data as IRepoCompany[];
const repoCompany = repoCompanieSet.find((repo: IRepoCompany) => {
return repo.id === repoCompanyId;
});
return repoCompany ? repoCompany.name : (props.repoCompanyId + '');
} else {
return "Loading...";
}
};
return (
<span data-test={props.repoCompanyId + '-rc-display'}>{RepoCompanyLookup(props.repoCompanyId)}</span>
)
};
export const _RepoCompanyDisplay = RepoCompanyDisplay;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(RepoCompanyDisplay);
@@ -0,0 +1,9 @@
import React from "react";
import ReactDOM from "react-dom";
import Footer from "./Footer";
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<Footer />, div);
ReactDOM.unmountComponentAtNode(div);
});
@@ -0,0 +1,30 @@
import "./Footer.css";
import {BUILD_STAMP, VERSION} from "../../environments/version";
import React from "react";
import watermark from "../../assets/light-house-corner-watermark.png";
const Footer: React.FC = () => {
const versionHint = VERSION.version + "-" + BUILD_STAMP;
const currentYear = new Date().getFullYear();
return (
<div>
<div className="watermark-outer">
<div className="watermark-inner">
<img src={watermark} className="watermark-primary" alt="" />
</div>
</div>
<footer className="py-3 bg-dark">
<div className="container">
<p
className="m-0 text-center text-white"
title={versionHint}>Copyright &copy; {currentYear} Landmark Credit Union
</p>
</div>
</footer>
</div>
);
};
export default Footer;
@@ -0,0 +1,15 @@
.form-input-label {
margin-bottom: 0;
font-size: 0.95em;
color: #555;
text-align: left;
}
.small-checkbox{
width: 1.5em;
height: 1.5em;
margin:0px;
padding:0px;
align-self:center;
margin-top: .25em;
}
@@ -0,0 +1,98 @@
import { shallow, mount } from "enzyme";
import React from "react";
import FormInput, { FormInputType } from "./FormInput";
describe("FormInput Component", () => {
it ("Renders Label", () => {
const wrapper = shallow(
<FormInput
label="The Label"
type={FormInputType.Selection}
value={1}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
const selectionInput = wrapper.find(`[data-test='label-text']`);
expect(selectionInput.length).toBe(1);
expect(wrapper.find(`[data-test='error-message']`).length).toBe(0);
});
it ("Passes Proper Error State To FormInputselector", () => {
const wrapper = mount(
<FormInput
label="The Label"
type={FormInputType.Selection}
value={1}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
expect(wrapper.state("isInputInErrorState")).toBe(false);
});
it ("Loads Required asterisk", () => {
const wrapper = mount(
<FormInput
required = {true}
label="The Label"
type={FormInputType.Selection}
value={undefined}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
expect(wrapper.find(`[data-test='required-asterisk']`).length).toBe(1);
expect(wrapper.state("isInputInErrorState")).toBe(false);
});
it ("Ignores labels if there is no label", () => {
const wrapper = mount(
<FormInput
required = {true}
type={FormInputType.Selection}
value={undefined}
options={[]}
onChange={() => {}}
placeholder="Option"
/>
);
expect(wrapper.find(`[data-test='form-input-label']`).length).toBe(0);
expect(wrapper.state("isInputInErrorState")).toBe(false);
});
it ("Turns red when hasError is set to true", () => {
const mountedWrapper = mount(
<FormInput
required={true}
type={FormInputType.Selection}
value={"undefined"}
options={[]}
onChange={() => {}}
placeholder="Option"
hasError={true}
/>
);
mountedWrapper.setProps(
{
value: "TEst"
}
);
mountedWrapper.update();
expect(mountedWrapper.state("isInputInErrorState")).toBe(true);
});
});
@@ -0,0 +1,128 @@
import { Moment } from "moment";
import React from "react";
import FormInputSelector from "./form-input-selector/FormInputSelector";
import "./FormInput.css";
export enum FormInputType {
Selection,
Datetime,
Text,
Textarea,
Number,
Checkbox
}
export enum FormInputNumericalType {
Monetary,
Percentage
}
export interface IFormInputSelectOption {
value: string | number;
display: string | number;
isDisabled?: boolean;
}
interface IFormInputWrapper<T1, T2> {
type: FormInputType;
value: T1;
label?: string;
placeholder?: string;
required?: boolean;
disabled?: boolean;
onChange: (newValue: T2) => any;
hasError?: boolean;
}
export interface FormInputText extends IFormInputWrapper<string | number | undefined, string> {
type: FormInputType.Text;
}
export interface FormInputTextArea extends IFormInputWrapper<string | number | undefined, string> {
type: FormInputType.Textarea;
}
export interface FormInputDate extends IFormInputWrapper<Moment | undefined, Moment | undefined> {
type: FormInputType.Datetime;
minDate?: Moment;
maxDate?: Moment;
}
export interface FormInputNumber extends IFormInputWrapper<number | undefined, number | undefined> {
type: FormInputType.Number;
precision?: number;
displayType?: FormInputNumericalType;
}
export interface FormInputSelection extends IFormInputWrapper<string | number | undefined, string> {
type: FormInputType.Selection;
options: IFormInputSelectOption[];
}
export interface FormInputCheckbox extends IFormInputWrapper<string | undefined, boolean> {
type: FormInputType.Checkbox;
checked?: boolean;
}
export type FormInputSpecs =
FormInputText |
FormInputTextArea |
FormInputDate |
FormInputSelection |
FormInputNumber |
FormInputCheckbox;
interface IFormInputState {
isInputInErrorState: boolean;
}
class FormInput extends React.Component<FormInputSpecs, IFormInputState> {
constructor(props: FormInputSpecs) {
super(props);
this.state = { isInputInErrorState: false };
this.setInputValidity = this.setInputValidity.bind(this);
}
componentDidUpdate(prevProps: FormInputSpecs) {
if (prevProps.value !== this.props.value) {
this.setInputValidity();
}
if(prevProps.hasError !== this.props.hasError){
this.setInputValidity();
}
}
setInputValidity() {
this.setState({
isInputInErrorState:
this.props.hasError === true ||
(!!this.props.required &&
(
this.props.value === null ||
this.props.value === undefined ||
this.props.value === ""
))
});
}
render() {
return (
<div>
{ this.props.label ?
<p className={"form-input-label " + (this.state.isInputInErrorState ? "text-danger" : "")}>
<span data-test="label-text">{this.props.label}</span>
{ this.props.required ?
<span className="text-danger">&nbsp;<b data-test="required-asterisk">*</b></span>
: null}
</p>
: null}
<FormInputSelector inputSpecs={this.props} isError={this.state.isInputInErrorState} onBlur={this.setInputValidity} />
</div>
);
}
}
export default FormInput;
@@ -0,0 +1,8 @@
@import "node_modules/bootstrap/scss/functions";
@import "node_modules/bootstrap/scss/variables";
@import "node_modules/bootstrap/scss/mixins";
.form-input-is-error {
border-width: 1px;
border-color: lighten($danger, 15%);
}
@@ -0,0 +1,217 @@
import { shallow } from "enzyme";
import moment, { Moment } from "moment";
import React from "react";
import DateFormatting from "../../../shared/formatting/DateFormatting";
import { FormInputDate, FormInputSelection, FormInputText, FormInputTextArea, FormInputType, FormInputCheckbox, FormInputNumber } from "../FormInput";
import FormInputSelector from "./FormInputSelector";
describe("FormInputSelector Component", () => {
it ("Renders and Updates Selection Input", () => {
const inputSpecs: FormInputSelection = {
type: FormInputType.Selection,
value: 1,
options: [
{ value: 1, display: "Option #1" },
{ value: 2, display: "Option #2" },
{ value: 3, display: "Option #3" }
],
onChange: (value) => callbackValue = value,
placeholder: "Select an Option"
};
let callbackValue: string | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let selectionInput = wrapper.find(`[data-test='select-input']`);
expect(selectionInput.length).toBe(1);
const selectionPlaceholderOption = wrapper.find(`[data-test='select-input-placeholder-option']`);
expect(selectionPlaceholderOption.length).toBe(1);
expect(selectionPlaceholderOption.text()).toEqual("Select an Option");
const selectionInputOptions = wrapper.find(`[data-test='select-input-option']`);
expect(selectionInputOptions.length).toBe(3);
expect(selectionInputOptions.at(0).text()).toEqual("Option #1");
expect(selectionInputOptions.at(1).text()).toEqual("Option #2");
expect(selectionInputOptions.at(2).text()).toEqual("Option #3");
expect(callbackValue).toBeUndefined();
selectionInput.simulate("change", { target: { value: "2" }});
expect(callbackValue).toEqual("2");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: 3
},
});
selectionInput = wrapper.find(`[data-test='select-input']`);
expect(selectionInput.props().value).toBe(3);
});
it ("Renders and Updates Text Input", () => {
const inputSpecs: FormInputText = {
type: FormInputType.Text,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: string | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let textInput = wrapper.find(`[data-test='text-input']`);
expect(textInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
textInput.simulate("change", { target: { value: "Different Text" }});
expect(callbackValue).toEqual("Different Text");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: "Even Newer Text"
},
});
textInput = wrapper.find(`[data-test='text-input']`);
expect(textInput.props().value).toBe("Even Newer Text");
});
it ("Renders and Updates Text Area Input", () => {
const inputSpecs: FormInputTextArea = {
type: FormInputType.Textarea,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: string | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let textAreaInput = wrapper.find(`[data-test='textarea-input']`);
expect(textAreaInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
textAreaInput.simulate("change", { target: { value: "Different Text" }});
expect(callbackValue).toEqual("Different Text");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: "Even Newer Text"
},
});
textAreaInput = wrapper.find(`[data-test='textarea-input']`);
expect(textAreaInput.props().value).toBe("Even Newer Text");
});
it ("Renders and Updates Date Input", () => {
const inputSpecs: FormInputDate = {
type: FormInputType.Datetime,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: Moment | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let dateInput = wrapper.find(`[data-test='date-input']`);
expect(dateInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
dateInput.simulate("change", new Date(2019, 1, 7));
expect(callbackValue).not.toBeUndefined();
expect(DateFormatting.FormatMMDDYYYY(callbackValue)).toEqual("02/07/2019");
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: moment("2015-08-24")
},
});
dateInput = wrapper.find(`[data-test='date-input']`);
expect(dateInput.props().selected).toEqual(moment("2015-08-24").toDate());
});
it("Renders and Updates Checkbox Input", () => {
let callbackValue : boolean | undefined;
const inputSpecs : FormInputCheckbox = {
type: FormInputType.Checkbox,
value: undefined,
checked: false,
onChange: (value) => callbackValue = value
}
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let checkboxinput = wrapper.find(`[data-test='checkbox-input']`);
checkboxinput.simulate("change", {target : {checked: true}});
expect(callbackValue).toBe(true);
wrapper.setProps({
inputSpecs: {
...inputSpecs,
checked: true
},
});
checkboxinput = wrapper.find(`[data-test='checkbox-input']`);
expect(checkboxinput.props().value).toBe("true");
expect(checkboxinput.props().checked).toBe(true)
});
it ("Renders and Updates Numerical Input", () => {
const inputSpecs: FormInputNumber = {
type: FormInputType.Number,
value: undefined,
onChange: (value) => callbackValue = value
};
let callbackValue: number | undefined;
let wrapper = shallow(
<FormInputSelector
inputSpecs={inputSpecs}
isError={false}
/>
);
let textInput = wrapper.find(`[data-test='number-input']`);
expect(textInput.length).toBe(1);
expect(callbackValue).toBeUndefined();
textInput.simulate("change", { target: { value: 5 }});
expect(callbackValue).toEqual(5);
wrapper.setProps({
inputSpecs: {
...inputSpecs,
value: 6
},
});
textInput = wrapper.find(`[data-test='number-input']`);
expect(textInput.props().value).toBe(6);
});
});
@@ -0,0 +1,184 @@
import { Moment } from "moment";
import React from "react";
import DatePicker from "react-datepicker";
import DateConversions from "../../../shared/conversions/DateConversions";
import NumericalFormatting from "../../../shared/formatting/NumericalFormatting";
import StringFormatting from "../../../shared/formatting/StringFormatting";
import { FormInputNumericalType, FormInputSpecs, FormInputType } from "../FormInput";
import "./FormInputSelector.scss";
interface IFormInputSelectorProps {
inputSpecs: FormInputSpecs;
isError: boolean;
onBlur?: () => any;
onFocus?: () => any;
}
const FormInputSelector: React.FC<IFormInputSelectorProps> = (props) => {
const handleBasicInputChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement>, callback: (data: string) => any) => {
callback(StringFormatting.TrimLeft(event.target.value));
};
const handleDateInputChange = (date: Date | null, callback: (data: Moment | undefined) => any) => {
const momentDate = DateConversions.dateToMoment(date);
callback(momentDate);
};
const numericalInputChange = (event: React.ChangeEvent<HTMLInputElement>, callback: (data: number | undefined) => any, precision?: number) => {
const value = event.target.value;
if (!value) {
callback(undefined);
return;
}
let floatValue = parseFloat(value);
if (precision) {
floatValue = NumericalFormatting.FormatPreciseFlooredNumber(floatValue, precision);
}
callback(floatValue);
};
const handleCheckboxInputChange = (event: React.ChangeEvent<HTMLInputElement>, callback: (data: boolean) => any) => {
callback(event.target.checked);
}
const getInputClassName = () => {
let classes = "form-control";
if (props.isError) {
classes += " form-input-is-error";
}
return classes;
};
const getAdjustedInputValue = (value: string | number | undefined) => {
if (value === null || value === undefined) {
return "";
}
return value;
};
switch(props.inputSpecs.type) {
case FormInputType.Selection:
const selectionOnChange = props.inputSpecs.onChange;
return (
<select
className={getInputClassName()}
value={getAdjustedInputValue(props.inputSpecs.value)}
onChange={(event) => handleBasicInputChange(event, selectionOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="select-input"
>
{ props.inputSpecs.placeholder ?
<option value="" disabled={true} data-test="select-input-placeholder-option">{props.inputSpecs.placeholder}</option>
: null}
{
props.inputSpecs.options.map((option, index) =>
<option
value={option.value}
key={index}
data-test="select-input-option"
>
{option.display}
</option>
)
}
</select>
);
case FormInputType.Textarea:
const textAreaOnChange = props.inputSpecs.onChange;
return (
<textarea
className={getInputClassName()}
placeholder={props.inputSpecs.placeholder}
value={props.inputSpecs.value}
onChange={(event) => handleBasicInputChange(event, textAreaOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="textarea-input"
/>
);
case FormInputType.Text:
const textOnChange = props.inputSpecs.onChange;
return (
<input
className={getInputClassName()}
placeholder={props.inputSpecs.placeholder}
value={props.inputSpecs.value}
onChange={(event) => handleBasicInputChange(event, textOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="text-input"
/>
);
case FormInputType.Datetime:
const datetimeOnChange = props.inputSpecs.onChange;
return (
<DatePicker
className={getInputClassName()}
placeholderText={props.inputSpecs.placeholder ? props.inputSpecs.placeholder : "mm/dd/yyyy"}
minDate={props.inputSpecs.minDate ? props.inputSpecs.minDate.toDate() : undefined}
selected={DateConversions.momentToDate(props.inputSpecs.value)}
disabled={props.inputSpecs.disabled}
onChange={(date) => handleDateInputChange(date, datetimeOnChange)}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="date-input"
/>
);
case FormInputType.Number:
const numberOnChange = props.inputSpecs.onChange;
const precision = props.inputSpecs.precision;
const step = precision ? Math.pow(10, - Math.floor(precision)).toString() : undefined;
return (
<div className="input-group">
{ props.inputSpecs.displayType === FormInputNumericalType.Monetary ?
<div className="input-group-prepend">
<span className="input-group-text">$</span>
</div>
: null}
<input
type="number"
step={step}
className={getInputClassName()}
placeholder={props.inputSpecs.placeholder}
value={props.inputSpecs.value}
onChange={(event) => numericalInputChange(event, numberOnChange, precision)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
data-test="number-input"
/>
{ props.inputSpecs.displayType === FormInputNumericalType.Percentage ?
<div className="input-group-append">
<span className="input-group-text">%</span>
</div>
: null}
</div>
);
case FormInputType.Checkbox:
const checkboxValueOnChange = props.inputSpecs.onChange;
return (
<input
value = {props.inputSpecs.checked? "true" : "false"}
className="pl-5 small-checkbox"
placeholder={props.inputSpecs.placeholder}
checked = {(props.inputSpecs.checked)}
onChange={(event) => handleCheckboxInputChange(event, checkboxValueOnChange)}
disabled={props.inputSpecs.disabled}
onBlur={props.onBlur}
onFocus={props.onFocus}
type = "checkbox"
data-test="checkbox-input"
/>
);
}
};
export default FormInputSelector;
@@ -0,0 +1,20 @@
.notification-bar {
.fa {
margin-right: 0.75em;
}
.notification-bar-button-text {
font-size: 1rem;
}
.notification-bar-sub-text-container {
margin-left: 1.75em;
hr {
margin: 2px;
width: 67%;
}
p {
font-size: 0.8rem;
margin: 0;
}
}
}
@@ -0,0 +1,74 @@
import React from 'react';
import { shallow } from 'enzyme';
import NotificationBar, { NotificationBarType } from './NotificationBar';
describe('NotificationBar', () => {
it ('Renders text', () => {
let messageText = "Text to display."
let wrapper = shallow(<NotificationBar text={messageText} type={NotificationBarType.Danger} />);
const component = wrapper.find(`[data-test='notification-bar']`)
expect(component.length).toBe(1);
expect(component.text()).toMatch(messageText);
});
it ('Renders Loading alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Loading} />);
const component = wrapper.find(`[data-test='loading-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Danger alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Danger} />);
const component = wrapper.find(`[data-test='danger-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Warning alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Warning} />);
const component = wrapper.find(`[data-test='warning-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Success alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Success} />);
const component = wrapper.find(`[data-test='success-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Info alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Info} />);
const component = wrapper.find(`[data-test='info-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Diabled alert', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Disabled} />);
const component = wrapper.find(`[data-test='exclamation-icon']`)
expect(component.length).toBe(1);
});
it ('Renders Dismiss Button', () => {
let wrapper = shallow(<NotificationBar text="" type={NotificationBarType.Success} isButtonPresent={true} />);
const component = wrapper.find(`[data-test='notification-bar-dismiss-button']`)
expect(component.length).toBe(1);
});
it ('Dismiss Button Triggers Callback', () => {
let valToUpdate = false;
let wrapper = shallow(
<NotificationBar
text=""
type={NotificationBarType.Success}
isButtonPresent={true}
onButtonClick={() => valToUpdate = true}
/>
);
expect(valToUpdate).toBe(false);
const component = wrapper.find(`[data-test='notification-bar-dismiss-button']`)
expect(component.length).toBe(1);
component.simulate('click');
expect(valToUpdate).toBe(true);
});
});
@@ -0,0 +1,96 @@
import React from 'react';
import './NotificationBar.scss';
export enum NotificationBarType {
Success = 'success',
Warning = 'warning',
Danger = 'danger',
Loading = 'primary',
Info = 'info',
Disabled = 'secondary'
}
interface INotificationBarProps {
type: NotificationBarType;
text: string;
subText?: string;
isButtonPresent?: boolean;
buttonText?: string;
fontAwesomeIcon?: string;
onButtonClick?: () => any;
}
const getNotificationIcon = (alertType: NotificationBarType, fontAwesomeIcon?: string) => {
if (fontAwesomeIcon) {
return <i className={'fa ' + fontAwesomeIcon} data-test="misc-icon"></i>
}
switch (alertType) {
case NotificationBarType.Loading:
return (
<i className="fa fa-spinner fa-spin" data-test="loading-icon"></i>
);
case NotificationBarType.Danger:
return (
<i className="fa fa-times-circle" data-test="danger-icon"></i>
);
case NotificationBarType.Warning:
return (
<i className="fa fa-exclamation-triangle" data-test="warning-icon"></i>
);
case NotificationBarType.Success:
return (
<i className="fa fa-check-circle" data-test="success-icon"></i>
);
case NotificationBarType.Info:
return (
<i className="fa fa-info-circle" data-test="info-icon"></i>
);
case NotificationBarType.Disabled:
return (
<i className="fa fa-exclamation-circle" data-test="exclamation-icon"></i>
);
}
};
const NotificationBar: React.FC<INotificationBarProps> = (props) => {
const callDismissCallback = () => {
if (props.onButtonClick) {
props.onButtonClick();
}
};
const getbuttonText = () => {
if (props.isButtonPresent) {
return props.buttonText || 'Dismiss';
}
return '';
}
return (
<div className={'notification-bar alert alert-' + props.type} role="alert" data-test="notification-bar">
{getNotificationIcon(props.type, props.fontAwesomeIcon)}
{props.text}
{props.isButtonPresent ?
<button
type="button"
className="close"
data-dismiss="alert"
aria-label="Close"
onClick={callDismissCallback}
data-test="notification-bar-dismiss-button"
>
<span className="notification-bar-button-text">{getbuttonText()}</span>
</button>
: null}
{props.subText ?
<div className="notification-bar-sub-text-container">
<hr></hr>
<p><b>{props.subText}</b></p>
</div>
: null}
</div>
)
};
export default NotificationBar;
@@ -0,0 +1,140 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IAllowedStatusChange } from "../../reducers/SettingsReducer";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import StatusChangeSelector from "./StatusChangeSelector";
describe("AccountPageHeader", () => {
let wrapper: ShallowWrapper;
const selectedAccount: IRepoAccount = {
repoId: 476,
accountNo: "123456",
status: "Original Status",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
branch: 0,
origAccountTypeCode: "",
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 0,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
currentDelinquentAmount: 19.45,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [{
repoId: 1,
individualId: "1",
ownershipType: 6,
individualName: "John Doe",
}],
collaterals: [],
repoStatusChanges: [],
};
const allowedStatusChanges: IAllowedStatusChange[] = [
{
id: 1,
currentStatus: "Original Status",
allowedChangeStatus: "Secondary Status",
},
{
id: 2,
currentStatus: "Original Status",
allowedChangeStatus: "Secondary Status (Alternative)",
},
{
id: 3,
currentStatus: "Separated Status",
allowedChangeStatus: "Final Status",
},
];
let statusToUpdate: string;
beforeEach(() => {
statusToUpdate = "";
wrapper = shallow(
<StatusChangeSelector
selectedRepoAccount={selectedAccount}
allowedStatusChanges={allowedStatusChanges}
onStatusChange={(status) => statusToUpdate = status}
/>
);
});
it ("Renders Correct Status Options", () => {
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("Original Status");
});
it ("Renders Correct Status Options on Prop Change", () => {
wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
status: "Separated Status",
},
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("Separated Status");
});
it ("Renders Empty Options on Missing Account", () => {
wrapper.setProps({
selectedRepoAccount: undefined,
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("");
});
it ("Renders Single Unknown Status", () => {
wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
status: "Non-Existant Status",
},
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.length).toBe(1);
expect(statusChangeSelect.props().value).toBe("Non-Existant Status");
});
it ("Select Callback Called", () => {
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusToUpdate).toEqual("");
statusChangeSelect.simulate("change", "Secondary Status");
expect(statusToUpdate).toEqual("Secondary Status");
});
it ("Does Not Render The Status Selector as Disabled When Account is Open", () => {
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.props().disabled).toBeFalsy();
});
it ("Renders Status Selector as Disabled When Account is Closed", () => {
wrapper = wrapper.setProps({
selectedRepoAccount: {
...selectedAccount,
dateClosed: moment("1916-04-29")
}
});
const statusChangeSelect = wrapper.find(`[data-test='status-change-selector']`);
expect(statusChangeSelect.props().disabled).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
import React from "react";
import { getViableStatuses, IAllowedStatusChange } from "../../reducers/SettingsReducer";
import RepoAccountService from "../../services/RepoAccountService";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import FormInput, { FormInputType } from "../form-input/FormInput";
interface IStatusChangeSelectorProps {
selectedRepoAccount?: IRepoAccount;
allowedStatusChanges: IAllowedStatusChange[];
onStatusChange: (newStatus: string) => any;
}
const StatusChangeSelector: React.FC<IStatusChangeSelectorProps> = (props) => {
const getAllowedStatusChanges = (): string[] => {
if (!props.selectedRepoAccount) {
return [];
}
return getViableStatuses(props.selectedRepoAccount.status, props.allowedStatusChanges);
};
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedRepoAccount);
return (
<FormInput
type={FormInputType.Selection}
value={props.selectedRepoAccount ? props.selectedRepoAccount.status : ""}
onChange={props.onStatusChange}
disabled={!props.selectedRepoAccount || isAccountClosed || props.selectedRepoAccount.status.toUpperCase() === "REDEEMED"}
options={getAllowedStatusChanges().map((status) => {
return {
value: status,
display: status
};
})}
data-test="status-change-selector"
/>
);
};
export default StatusChangeSelector;
@@ -0,0 +1,7 @@
.nav-link {
cursor: pointer;
}
.nav-item p {
margin-bottom: 0;
}
@@ -0,0 +1,61 @@
import React from 'react';
import { ShallowWrapper, shallow } from "enzyme";
import TabView, { ITabViewPage as ITabViewPage } from "./TabView";
describe('TabView component', () => {
let wrapper: ShallowWrapper;
const pages: ITabViewPage[] = [
{
header: 'Page 1',
element: <div data-test="page-0-element"></div>,
},
{
header: 'Page 1',
element: <div data-test="page-1-element"></div>,
},
]
beforeEach(() => {
wrapper = shallow(<TabView pages={pages}/>);
})
it ('Renders initial page only', () => {
const pageZeroComponent = wrapper.find(`[data-test='page-0-element']`);
expect(pageZeroComponent.length).toBe(1);
const pageOneComponent = wrapper.find(`[data-test='page-1-element']`);
expect(pageOneComponent.length).toBe(0);
})
it ('Renders secondary page only on click', () => {
const navLinkComponent = wrapper.find(`[data-test='nav-link-1']`);
navLinkComponent.simulate('click');
const pageOneComponent = wrapper.find(`[data-test='page-1-element']`);
expect(pageOneComponent.length).toBe(1);
const pageZeroComponent = wrapper.find(`[data-test='page-0-element']`);
expect(pageZeroComponent.length).toBe(0);
})
it ('Calls Tab Callback on Tab Change', () => {
let tabOneValue = 0;
let tabTwoValue = 5;
const tabViewPages: ITabViewPage[] = [
{
header: 'Page 1',
element: <div data-test="page-0-element"></div>,
tabOpenCallback: () => {tabOneValue = 42},
},
{
header: 'Page 1',
element: <div data-test="page-1-element"></div>,
tabOpenCallback: () => {tabTwoValue = 1945}
},
];
wrapper = shallow(<TabView pages={tabViewPages} />);
expect(tabOneValue).toBe(42);
const navLinkComponent = wrapper.find(`[data-test='nav-link-1']`);
navLinkComponent.simulate('click');
expect(tabTwoValue).toBe(1945);
})
});
@@ -0,0 +1,76 @@
import React, { ReactElement } from "react";
import "./TabView.css";
export interface ITabViewPage {
header: string;
element: ReactElement;
tabOpenCallback?: () => any;
}
export interface TabViewProps {
pages: ITabViewPage[];
}
interface TabViewState {
currentPageIndex: number;
}
class TabView extends React.Component<TabViewProps, TabViewState> {
constructor(props: TabViewProps) {
super(props);
this.state = {
currentPageIndex: 0,
};
this.callPageCallback(0);
}
private getClassStyle(index: number): string {
return index === this.state.currentPageIndex ? "active ": "text-primary ";
}
private setCurrentPage(index: number): void {
const newState: TabViewState = Object.assign({}, this.state);
newState.currentPageIndex = index;
this.setState(newState);
this.callPageCallback(index);
}
private getCurrentPageElement(): ReactElement {
const currentPage = this.props.pages[this.state.currentPageIndex];
if (currentPage) {
return currentPage.element;
}
return (<h3>Missing</h3>);
}
private callPageCallback(index: number) {
if (index < this.props.pages.length) {
const tabPageCallback = this.props.pages[index].tabOpenCallback;
if (tabPageCallback !== undefined) {
tabPageCallback();
}
}
}
public render() {
return (
<div>
<ul className="nav nav-tabs">
{
this.props.pages.map((page, index) => {
return (
<li key={index} className="nav-item">
<p className={"nav-link " + this.getClassStyle(index)} onClick={() => this.setCurrentPage(index)} data-test={"nav-link-" + index}>{page.header}</p>
</li>
);
})
}
</ul>
{this.getCurrentPageElement()}
</div>
);
}
}
export default TabView;
@@ -0,0 +1,16 @@
.table-pagination p {
color: white;
margin: 0.5em 1em;
}
.table-pagination .table-chev:not(.table-chev-disabled) {
cursor: pointer;
}
.table-pagination .table-chev:hover:not(.table-chev-disabled) {
opacity: 0.5;
}
.table-pagination .table-chev-disabled {
opacity: 0.25;
}
@@ -0,0 +1,59 @@
import React from 'react';
import './TablePagination.css';
import ITablePagination from "./TablePagination";
import { shallow } from "enzyme";
import TablePagination from './TablePagination';
import { isExpressionWithTypeArguments } from 'typescript';
describe("FormInput Component", () => {
it ("Only Allows Next/Prev page when enabled 1", () => {
let nextPage = false;
let prevPage = false;
let wrapper = shallow(<TablePagination
totalItems = {10}
itemsPerPage = {2}
totalPages = {5}
currentPage = {0}
isPrevDisabled = {true}
isNextDisabled = {false}
nextPageCallback = {() => {nextPage = !nextPage}}
prevPageCallback = {() => {prevPage = !prevPage}}
/>)
const prevlink = wrapper.find(`[data-test='prev-page-link']`);
expect(prevlink.hasClass('table-chev-disabled')).toBe(true);
prevlink.simulate("click")
expect(prevPage).toBe(false)
const nextLink = wrapper.find(`[data-test='next-page-link']`);
expect(nextLink.hasClass('table-chev-disabled')).toBe(false);
nextLink.simulate("click")
expect(nextPage).toBe(true)
});
it ("Only Allows Next/Prev page when enabled 2", () => {
let nextPage = false;
let prevPage = false;
let wrapper = shallow(<TablePagination
totalItems = {10}
itemsPerPage = {2}
totalPages = {5}
currentPage = {2}
isPrevDisabled = {false}
isNextDisabled = {true}
nextPageCallback = {() => {nextPage = !nextPage}}
prevPageCallback = {() => {prevPage = !prevPage}}
/>)
const prevlink = wrapper.find(`[data-test='prev-page-link']`);
expect(prevlink.hasClass('table-chev-disabled')).toBe(false);
prevlink.simulate("click")
expect(prevPage).toBe(true)
const nextLink = wrapper.find(`[data-test='next-page-link']`);
expect(nextLink.hasClass('table-chev-disabled')).toBe(true);
nextLink.simulate("click")
expect(nextPage).toBe(false)
});
});
@@ -0,0 +1,51 @@
import React from 'react';
import './TablePagination.css';
interface ITablePaginationProps {
totalItems: number;
itemsPerPage: number;
currentPage: number;
totalPages: number;
isPrevDisabled: boolean;
isNextDisabled: boolean;
nextPageCallback: () => any;
prevPageCallback: () => any;
}
const TablePagination: React.FC<ITablePaginationProps> = (props) => {
const disabledChevClassName = 'table-chev-disabled';
return (
<div className="table-pagination bg-secondary">
<div className="row">
<div className="col">
<p
data-test='prev-page-link'
className={'table-chev table-chev-left ' + (props.isPrevDisabled ? disabledChevClassName : '')}
onClick={() => !props.isPrevDisabled ? props.prevPageCallback() : null}
>
<i className="fa fa-chevron-left" aria-hidden="true"></i>
</p>
</div>
<div className="col">
<p className="text-center">
Page <b>{props.currentPage}</b> of <b>{props.totalPages}</b>.
&nbsp;
Showing <b>{props.itemsPerPage}</b> of <b>{props.totalItems}</b>
</p>
</div>
<div className="col">
<p
data-test='next-page-link'
className={'text-right table-chev table-chev-right ' + (props.isNextDisabled ? disabledChevClassName : '')}
onClick={() => !props.isNextDisabled ? props.nextPageCallback() : null}
>
<i className="fa fa-chevron-right" aria-hidden="true"></i>
</p>
</div>
</div>
</div>
)
}
export default TablePagination;
@@ -0,0 +1,11 @@
.warning-info-page {
padding-top: 15em;
h1 {
text-align: center;
}
p {
font-weight: 100;
text-align: center;
}
}
@@ -0,0 +1,19 @@
import React from "react";
import "./WarningInfoPage.scss";
interface IWarningInfoPageProps {
title: string;
body?: string;
}
const WarningInfoPage: React.FC<IWarningInfoPageProps> = (props) => {
return (
<div className="warning-info-page">
<h1 aria-hidden="true"><i className="fa fa-exclamation-triangle" /></h1>
<h1>{props.title}</h1>
<p>{props.body}</p>
</div>
);
};
export default WarningInfoPage;
@@ -0,0 +1,13 @@
import React from "react";
import WarningInfoPage from "../../components/warning-info-page/WarningInfoPage";
const AccessDeniedPage: React.FC = () => {
return (
<WarningInfoPage
title="Access Denied"
body="Please contact IT Operations if this is an error."
/>
);
};
export default AccessDeniedPage;
@@ -0,0 +1,9 @@
.account-activity {
padding-top: 1em;
}
.account-activity-notable-dates {
h5 {
margin-top: 0.5em;
}
}
@@ -0,0 +1,101 @@
import { shallow, ShallowWrapper } from "enzyme";
import { CustomAsyncNotification } from "lcu-react-lib";
import moment from "moment";
import React from "react";
import { HistorySource, HistoryType, ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
import { ICollateral, IRepoAccount, IRepoStatusChange } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {_AccountActivity} from "./AccountActivity";
describe("AccountActivity component", () => {
let wrapper: ShallowWrapper;
const accountHistories: IAsyncStateContainer<ITemenosHistory[]> = {
state: AsyncState.Success,
data: [{
Source: HistorySource.Account,
Name: "status",
Created: moment("1997-02-05"),
Type: HistoryType.Comment,
UserId: 0,
Message: "Mesage"
},
{
Source: HistorySource.Account,
Name: "status",
Created: moment("1997-02-05"),
Type: HistoryType.Comment,
UserId: 0,
Message: "Mesage"
},
{
Source: HistorySource.Account,
Name: "status",
Created: moment("1997-02-05"),
Type: HistoryType.Letter,
UserId: 0,
Message: "Mesage"
}]
};
const caseHistories: IAsyncStateContainer<ITemenosHistory[]> = {
state: AsyncState.Success,
data: []
};
beforeEach(() => {
wrapper = shallow(
<_AccountActivity
selectedAccount={{
accountNo: "1234",
collaterals: [] as ICollateral[],
repoStatusChanges: [] as IRepoStatusChange[],
} as IRepoAccount}
alerts={[]}
actions={[]}
markAlertRead={()=> {}}
accountHistories={accountHistories}
caseHistories={caseHistories}
saveRepoAccount={() => {}}
removeAccountFromAlertFilter={()=>{}}
externalUserMap={[]}
/>);
});
it("Displays all components", () => {
expect(wrapper.find(`[data-test='case-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='account-notification']`).length).toBe(1);
expect(wrapper.find(`[data-test='repo-letters']`).length).toBe(1);
});
it("Filters for only letters history for alerts and table", ()=>{
expect(wrapper.find(`[data-test='case-notification']`).prop("container").data.length).toBe(0);
expect(wrapper.find(`[data-test='account-notification']`).prop("container").data.length).toBe(1);
expect(wrapper.find(`[data-test='repo-letters']`).length).toBe(1);
});
it("Displays Custom Info Notification When Response Array is Emtpy", () => {
var customCaseNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='case-notification']`).prop("customNotifications");
expect(customCaseNotification).not.toBe(null);
expect(customCaseNotification[0].displayWhen(caseHistories.data)).toBe(true);
var customAccountNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='account-notification']`).prop("customNotifications");
expect(customAccountNotification).not.toBe(null);
expect(customAccountNotification[0].displayWhen(accountHistories.data)).toBe(false);
});
it("Displays Custom Info Notification When Response Array is Emtpy", () => {
//Previous Test With Data Flipped to ensure both have expected behaviour
var customCaseNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='case-notification']`).prop("customNotifications");
expect(customCaseNotification).not.toBe(null);
expect(customCaseNotification[0].displayWhen(accountHistories.data)).toBe(false);
var customAccountNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='account-notification']`).prop("customNotifications");
expect(customAccountNotification).not.toBe(null);
expect(customAccountNotification[0].displayWhen(caseHistories.data)).toBe(true);
});
});
@@ -0,0 +1,125 @@
import {AsyncNotification, NotificationType} from "lcu-react-lib";
import React from "react";
import { connect } from "react-redux";
import { markAlertRead } from "../../actions/AlertActions";
import { putRepoAccount, removeRepoAccountFromAlertFilter } from "../../actions/RepoAccountActions";
import { IAlert } from "../../reducers/AlertReducer";
import { IExternalUser } from "../../reducers/SettingsReducer";
import { HistoryType, ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import HistoryTable from "../history-table/HistoryTable";
import AccountActivityNotifications from "./account-activity-notifications/AccountActivityNotifications";
import "./AccountActivity.scss";
import NotableDates from "./notable-dates/NotableDates";
interface IAccountActivityProps {
selectedAccount: IRepoAccount;
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
externalUserMap: IExternalUser[];
}
interface IPropsFromState {
alerts: IAlert[];
actions: IAlert[];
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
alerts: state.alertState.alerts,
actions: state.alertState.actions,
};
};
interface IPropsFromDispatch {
markAlertRead: (alert: IAlert) => any;
saveRepoAccount: (repo: IRepoAccount) => any;
removeAccountFromAlertFilter: (alertMessage: string, repoId: number) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
markAlertRead: markAlertRead,
saveRepoAccount: putRepoAccount,
removeAccountFromAlertFilter: removeRepoAccountFromAlertFilter
};
type Props = IPropsFromState & IAccountActivityProps & IPropsFromDispatch;
const AccountActivity: React.FC<Props> = (props) => {
const filterOnlyAccountAlerts = (alerts: IAlert[]): IAlert[] => {
return alerts.filter((alert) => alert.repoId === props.selectedAccount.repoId);
};
const dismissAlert = (alert: IAlert) => {
props.markAlertRead(alert);
props.removeAccountFromAlertFilter(alert.message, alert.repoId);
};
const filterOnlyLettersHistories = (histories : IAsyncStateContainer<ITemenosHistory[]>) : IAsyncStateContainer<ITemenosHistory[]> => {
if(histories.data){
return {
state: histories.state,
data: histories.data.filter(x => x.Type === HistoryType.Letter)
};
}
return histories;
};
return (
<div className="account-activity container">
<AccountActivityNotifications
accountAlerts={filterOnlyAccountAlerts(props.alerts)}
accountActions={filterOnlyAccountAlerts(props.actions)}
onAlertDismiss={dismissAlert}
data-test="account-activity-notifications"
/>
<div className="row">
<div className="col-sm-8">
<h5>Repo History - Letters</h5>
<AsyncNotification
container={filterOnlyLettersHistories(props.accountHistories)}
subject={"Account Letter Histories"}
data-test={"account-notification"}
customNotifications={
[
{
displayWhen: (data: ITemenosHistory[]) => {return data.length < 1;},
message: `No Account Letters for ${props.selectedAccount.accountNo}`,
type: NotificationType.Info
}
]
}
/>
<AsyncNotification
container={filterOnlyLettersHistories(props.caseHistories)}
subject={"Case Letter Histories"}
data-test={"case-notification"}
customNotifications={
[
{
displayWhen: (data: ITemenosHistory[]) => {return data.length < 1;},
message: `No Case Letters for ${props.selectedAccount.accountNo}`,
type: NotificationType.Info
}
]
}
/>
<HistoryTable
accountNo={props.selectedAccount.accountNo}
caseHistories={filterOnlyLettersHistories(props.caseHistories)}
accountHistories={filterOnlyLettersHistories(props.accountHistories)}
externalUserMap={props.externalUserMap}
data-test="repo-letters"
/>
</div>
<NotableDates selectedAccount={props.selectedAccount} saveRepoAccount={props.saveRepoAccount} />
</div>
</div>
);
};
export const _AccountActivity = AccountActivity;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AccountActivity);
@@ -0,0 +1,44 @@
import React from 'react';
import { ShallowWrapper, shallow } from 'enzyme';
import AccountActivityNotifications from './AccountActivityNotifications';
import { IAlert } from '../../../reducers/AlertReducer';
describe('AccountActivityNotifications Component', () => {
let wrapper: ShallowWrapper;
const dummyAlerts: IAlert[] = [
{
id: 1,
message: "Message",
} as IAlert,
{
id: 1,
message: "Message",
} as IAlert,
];
const dummyActions: IAlert[] = [
{
id: 1,
message: "Message",
} as IAlert,
];
beforeEach(() => {
wrapper = shallow(
<AccountActivityNotifications
accountAlerts={dummyAlerts}
accountActions={dummyActions}
/>
);
});
it ('Renders Alerts', () => {
const alerts = wrapper.find(`[data-test='account-activity-notifications-alert']`);
expect(alerts.length).toBe(2);
});
it ('Renders Actions', () => {
const actions = wrapper.find(`[data-test='account-activity-notifications-action']`);
expect(actions.length).toBe(1);
});
});
@@ -0,0 +1,44 @@
import React from 'react';
import { IAlert } from '../../../reducers/AlertReducer';
import NotificationBar, { NotificationBarType } from '../../../components/notification-bar/NotificationBar';
interface IAcccountActivityNotificationsProps {
accountAlerts: IAlert[];
accountActions: IAlert[];
onAlertDismiss: (alert: IAlert) => any;
}
const AccountActivityNotifications: React.FC<IAcccountActivityNotificationsProps> = (props) => {
return (
<div className="account-activity-notifications">
{
props.accountActions.map((action) =>
<NotificationBar
key={action.id + '-' + action.message}
type={NotificationBarType.Info}
text={action.message}
subText={action.subMessage}
fontAwesomeIcon="fa-exclamation"
data-test="account-activity-notifications-action"
/>
)
}
{
props.accountAlerts.map((alert) =>
<NotificationBar
key={alert.id + '-' + alert.message}
type={NotificationBarType.Warning}
text={alert.message}
subText={alert.subMessage}
isButtonPresent={true}
onButtonClick={() => props.onAlertDismiss(alert)}
fontAwesomeIcon="fa-bell"
data-test="account-activity-notifications-alert"
/>
)
}
</div>
);
};
export default AccountActivityNotifications;
@@ -0,0 +1,11 @@
.notable-dates {
padding-top: 1em;
}
.notable-dates-edit {
margin-left: 2.5em;
}
.date-picker {
margin-top: 1em;
}
@@ -0,0 +1,76 @@
import React from "react";
import { ShallowWrapper, shallow } from "enzyme";
import NotableDates from "./NotableDates";
import { IRepoAccount, ICollateral, IRepoStatusChange } from '../../../services/service-types/RepoAccountTypes';
import moment from "moment";
describe('Notable Dates component', () => {
let wrapper: ShallowWrapper;
const dummyRepoAccount = {
accountNo: '1234',
collaterals: [] as ICollateral[],
repoStatusChanges: [] as IRepoStatusChange[],
} as IRepoAccount
let dummyUpdatedRepoAccount: IRepoAccount | undefined;
beforeEach(() => {
wrapper = shallow(
<NotableDates
selectedAccount={dummyRepoAccount}
saveRepoAccount={(x) => dummyUpdatedRepoAccount = x}
/>);
})
it ('Renders Existing Dates', () => {
wrapper = wrapper.setProps({
selectedAccount: {
o4RDate: moment('1945-05-07'),
redemptionExpiredDate: moment('1945-09-02'),
collaterals: [
{},
{ retitledInLandmarkName: moment('1914-07-28') },
],
repoStatusChanges: [
{ statusTo: 'SOLD', changedDate: moment('1865-04-12') },
{ statusTo: 'REPO', changedDate: moment('1918-11-11') }
]
}
});
const o4rDate = wrapper.find(`[data-test='notable-dates-o4r-date']`);
expect(o4rDate.length).toBe(1);
expect(o4rDate.text()).toContain('05/07/1945');
const repoDate = wrapper.find(`[data-test='notable-dates-repo-date']`);
expect(repoDate.length).toBe(1);
expect(repoDate.text()).toContain('11/11/1918');
const redemDate = wrapper.find(`[data-test='notable-dates-redem-exp-date']`);
expect(redemDate.length).toBe(1);
expect(redemDate.text()).toContain('09/02/1945');
const retitledDate = wrapper.find(`[data-test='notable-dates-retitled-date']`);
expect(retitledDate.length).toBe(1);
expect(retitledDate.text()).toContain('07/28/1914');
});
it ('Renders Non-Existing Dates', () => {
const o4rDate = wrapper.find(`[data-test='notable-dates-o4r-date']`);
expect(o4rDate.length).toBe(1);
expect(o4rDate.text()).toContain('N/A');
const repoDate = wrapper.find(`[data-test='notable-dates-repo-date']`);
expect(repoDate.length).toBe(1);
expect(repoDate.text()).toContain('N/A');
const redemDate = wrapper.find(`[data-test='notable-dates-redem-exp-date']`);
expect(redemDate.length).toBe(1);
expect(redemDate.text()).toContain('N/A');
const retitledDate = wrapper.find(`[data-test='notable-dates-retitled-date']`);
expect(retitledDate.length).toBe(1);
expect(retitledDate.text()).toContain('N/A');
});
});
@@ -0,0 +1,121 @@
import React from "react";
import { ICollateral, IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import RepoAccountService from "../../../services/RepoAccountService";
import { Moment } from "moment";
import DateFormatting from "../../../shared/formatting/DateFormatting";
import DatePicker from 'react-datepicker';
import DateConversions from "../../../shared/conversions/DateConversions";
import "./NotableDates.css";
const cloneDeep = require('lodash.clonedeep');
interface INotableDatesProps {
selectedAccount: IRepoAccount;
saveRepoAccount: (repo: IRepoAccount) => any;
}
interface INotableDatesState {
repoAccount: IRepoAccount;
isOpen: boolean;
}
type IRepoAccountUpdater<T> = (repoAccount: IRepoAccount, value: T) => void;
class NotableDates extends React.Component<INotableDatesProps, INotableDatesState> {
constructor(props: INotableDatesProps) {
super(props);
this.state = {
repoAccount: cloneDeep(props.selectedAccount),
isOpen: false
}
}
private applyChange<T>(value: T, updater: IRepoAccountUpdater<T>) {
const copiedAccount = cloneDeep(this.state.repoAccount);
updater(copiedAccount, value);
this.setState({
repoAccount: copiedAccount,
});
}
private handleDateChange(date: Date | null, updater: IRepoAccountUpdater<Moment | undefined>) {
const momentDate = DateConversions.dateToMoment(date);
this.applyChange(momentDate, updater);
}
toggleOpen = () => this.setState({ isOpen: !this.state.isOpen });
private formatDate = (date: Moment | undefined = undefined): string => {
return date ? DateFormatting.FormatMMDDYYYY(date) : "N/A";
};
private getRetitledDateString = (collaterals: ICollateral[]): string => {
for (const collateral of collaterals) {
if (collateral.retitledInLandmarkName) {
return this.formatDate(collateral.retitledInLandmarkName);
}
}
return this.formatDate();
};
private getRepoDate = (repoAccount: IRepoAccount): string => {
return this.formatDate(RepoAccountService.GetLatestStatusChangeDate("REPO", repoAccount));
};
private editButton = () => {
return (
<button
className="notable-dates-edit btn btn-info"
onClick={this.toggleOpen}
data-test="notable-dates-edit-retitled-date-btn"
>
Edit
</button>
);
};
render() {
return (
<div className="col-sm-4 card notable-dates ml">
<h5>Notable Dates:</h5>
<hr />
<p><b>O4R:</b>
<span data-test="notable-dates-o4r-date"> {this.formatDate(this.props.selectedAccount.o4RDate)}</span>
</p>
<p><b>Repo:</b>
<span data-test="notable-dates-repo-date"> {this.getRepoDate(this.props.selectedAccount)}</span>
</p>
<p><b>Redemption Expired:</b>
<span data-test="notable-dates-redem-exp-date"> {this.formatDate(this.props.selectedAccount.redemptionExpiredDate)}
</span>
{this.state.isOpen ?
<div>
<div className="form-group">
<DatePicker
className="form-control date-picker"
placeholderText="mm/dd/yyyy"
selected={DateConversions.momentToDate(this.state.repoAccount.redemptionExpiredDate)}
onChange={(date) => this.handleDateChange(date, (a, v) => a.redemptionExpiredDate = v)}
data-test={"notable-dates-redem-exp-date-picker"}
/>
</div>
<div className="form-group">
<button
className="form-control btn btn-info auction-panel-btn"
onClick={() => { this.props.saveRepoAccount(this.state.repoAccount) }}
data-test="notable-dates-redem-exp-submit-btn">
Submit
</button>
</div>
</div>
: null}
</p>
<p><b>Retitled:</b>
<span data-test="notable-dates-retitled-date"> {this.getRetitledDateString(this.props.selectedAccount.collaterals)}</span>
</p>
</div>
);
}
}
export default NotableDates;
@@ -0,0 +1,229 @@
import {shallow, ShallowWrapper} from "enzyme";
import moment, {Moment} from "moment";
import React from "react";
import {IRepoAccount} from "../../services/service-types/RepoAccountTypes";
import DateCalculations from "../../shared/calculations/DateCalculations";
import AccountDetails from "./AccountDetails";
describe("AccountDetails Container", () => {
let wrapper: ShallowWrapper;
const dummyAccount: IRepoAccount = {
repoId: 476,
accountNo: "string",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
origAccountTypeCode: "",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 19.45,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
writtenOffDate: moment("1492-01-01"),
borrowers: [],
collaterals: [],
repoStatusChanges: [],
};
beforeEach(() => {
wrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
});
it("Renders Select Account Fields Correctly", () => {
const repoDbDate = wrapper.find(`[data-test='repo-db-date']`);
const firstDelinquent = wrapper.find(`[data-test='first-delinquent']`);
const otherRepos = wrapper.find(`[data-test='other-repos']`);
const daysDelqWhenAdded = wrapper.find(`[data-test='days-delq-when-added']`);
const origAmount = wrapper.find(`[data-test='orig-amount']`);
const balance = wrapper.find(`[data-test='balance']`);
const lastPayment = wrapper.find(`[data-test='last-payment']`);
const daysInStatus = wrapper.find(`[data-test='days-in-status']`);
const daysCurrentDelq = wrapper.find(`[data-test='days-current-delq']`);
const accountCode = wrapper.find(`[data-test='account-code']`);
const branch = wrapper.find(`[data-test='branch']`);
const currentDelqAmount = wrapper.find(`[data-test='current-delq-amount']`);
const lastPmtAmount = wrapper.find(`[data-test='last-pmt-amount']`);
const requiredAmount = wrapper.find(`[data-test='required-amount']`);
const bankruptcy = wrapper.find(`[data-test='bankruptcy']`);
expect(repoDbDate.length).toBe(1);
expect(repoDbDate.props().value).toEqual("05/07/1945");
expect(firstDelinquent.length).toBe(1);
expect(firstDelinquent.props().value).toEqual("11/11/1918");
expect(otherRepos.length).toBe(1);
expect(otherRepos.props().value).toEqual("0");
expect(daysDelqWhenAdded.length).toBe(1);
expect(daysDelqWhenAdded.props().value).toEqual(101);
expect(origAmount.length).toBe(1);
expect(origAmount.props().value).toEqual("$42.00");
expect(balance.length).toBe(1);
expect(balance.props().value).toEqual("$0.00");
expect(lastPayment.length).toBe(1);
expect(lastPayment.props().value).toEqual("04/15/1865");
expect(daysInStatus.length).toBe(1);
expect(daysInStatus.props().value).toEqual("Missing");
expect(daysCurrentDelq.length).toBe(1);
expect(daysCurrentDelq.props().value).toEqual(
DateCalculations.DayDifference(moment("1941-12-07"), moment())
);
expect(accountCode.length).toBe(1);
expect(accountCode.props().value).toEqual("(-) / acct-type-code");
expect(branch.length).toBe(1);
expect(branch.props().value).toEqual(0);
expect(currentDelqAmount.length).toBe(1);
expect(currentDelqAmount.props().value).toEqual("$19.45");
expect(lastPmtAmount.length).toBe(1);
expect(lastPmtAmount.props().value).toEqual("$1,912.00");
expect(requiredAmount.length).toBe(1);
expect(requiredAmount.props().value).toEqual("$3.14");
expect(bankruptcy.length).toBe(1);
expect(bankruptcy.props().value).toEqual("No");
});
it("Written Off Date Displays No", () => {
const dummyAccount: IRepoAccount = {
repoId: 476,
accountNo: "string",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
origAccountTypeCode: "",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 19.45,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [],
collaterals: [],
repoStatusChanges: [],
};
wrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const writtenOffDate = wrapper.find(`[data-test='written-off']`);
expect(writtenOffDate.length).toBe(1);
expect(writtenOffDate.props().value).toBe("No");
});
it("Written Off Date Displays Date", () => {
wrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const writtenOffDate = wrapper.find(`[data-test='written-off']`);
expect(writtenOffDate.length).toBe(1);
expect(writtenOffDate.props().value).toBe("01/01/1492");
});
it("Renders Account Code with Original Account Code Properly", () => {
dummyAccount.origAccountTypeCode = "orig-code";
const tempwrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const accountCode = tempwrapper.find(`[data-test='account-code']`);
expect(accountCode.props().value).toBe("(orig-code) / acct-type-code");
});
it("Renders the Branch Name when a mapping is available", () => {
const tempwrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[{branchId: 0, branchName: "Milwaukee"}]}
refreshCollateral={() => {
}}
/>
);
const branch = tempwrapper.find(`[data-test='branch']`);
expect(branch.props().value).toBe("Milwaukee");
});
it("Renders the Branch Number when branchMapping is undefined", () => {
const tempwrapper = shallow(
<AccountDetails
selectedAccount={dummyAccount}
otherRepos={"0"}
branchMapping={[]}
refreshCollateral={() => {
}}
/>
);
const branch = tempwrapper.find(`[data-test='branch']`);
expect(branch.props().value).toBe(0);
});
});
@@ -0,0 +1,170 @@
import moment from "moment";
import React from "react";
import CollateralDisplay, { ICollateralDisplay } from "../../components/collateral-display/CollateralDisplay";
import { IBranchSetEntry } from "../../reducers/SettingsReducer";
import RepoAccountService from "../../services/RepoAccountService";
import { ICollateral, IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import DateCalculations from "../../shared/calculations/DateCalculations";
import DateFormatting from "../../shared/formatting/DateFormatting";
import MonetaryFormatting from "../../shared/formatting/MonetaryFormatting";
interface IAccountDetailsProps {
selectedAccount?: IRepoAccount;
otherRepos: string;
refreshCollateral: (collateral: ICollateral, memberNo: number, loanNo: number) => any;
branchMapping: IBranchSetEntry[];
}
const AccountDetails: React.FC<IAccountDetailsProps> = (props) => {
const getWrittenOffText = () => {
const writtenOffDate = getAccountField(acc => acc.writtenOffDate);
if (writtenOffDate) {
return DateFormatting.FormatMMDDYYYY(writtenOffDate);
}
return "No";
};
const getAccountField = function<T>(valuePlucker: (repoAccount: IRepoAccount) => T): T | undefined {
if (props.selectedAccount) {
return valuePlucker(props.selectedAccount);
}
};
const getControlledAccountField = function<T>(valuePlucker: (repoAccount: IRepoAccount) => T): T | string {
const pluckedVal = getAccountField(valuePlucker);
return pluckedVal !== undefined ? pluckedVal : "";
};
const getDaysCurrentDelq = (): string | number => {
const dueToPayDate = getAccountField(acc => acc.dueToPayDate);
if (dueToPayDate) {
return DateCalculations.DayDifference(dueToPayDate, moment());
}
return "";
};
const formatAccountTypeCode = (): string => {
const accountTypeCode = getControlledAccountField(acc => acc.accountTypeCode);
let origAccountTypeCode = getControlledAccountField(acc => acc.origAccountTypeCode);
origAccountTypeCode = !origAccountTypeCode ? "-" : origAccountTypeCode;
return "("+origAccountTypeCode+") / " + accountTypeCode;
};
const createCollateralDisplays = (): ICollateralDisplay[] => {
if (props.selectedAccount) {
return props.selectedAccount.collaterals.map((collateral) => {
return {
collateral: collateral,
refreshCollateral: () => {
return props.refreshCollateral(collateral, props.selectedAccount!.memberNo, props.selectedAccount!.loanNo);
},
};
});
} else {
return [];
}
};
const getBranchName = (): string | number =>{
const BranchIdFromAccount = getControlledAccountField(acc => acc.branch);
if(!props.branchMapping){
return BranchIdFromAccount;
}
// eslint-disable-next-line eqeqeq
const mappedEntry = props.branchMapping.find(branch => branch.branchId == BranchIdFromAccount);
return (mappedEntry && mappedEntry.branchName) ? mappedEntry.branchName : BranchIdFromAccount;
};
const getSixtyPlusCollector = (): string | undefined =>{
const SixtyPlusCollector = getAccountField(acct => acct.sixtyPlusCollector);
if(SixtyPlusCollector){
return SixtyPlusCollector.sixtyPlusCollectorName;
}
return "";
}
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedAccount);
return (
<div className="account-details-holder container">
<CollateralDisplay isAccountClosed={isAccountClosed} collateral={createCollateralDisplays()} />
<div className="account-page-input-section row">
<div className="col-sm">
<p>Repo DB Date</p>
<input data-test="repo-db-date" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.o4RDate))} disabled={true} />
<p>Orig Amount</p>
<input data-test="orig-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.origAmount))} disabled={true} />
<p>Account Code</p>
<input data-test="account-code" type="text" className="form-control" value={formatAccountTypeCode()} disabled={true} />
<p>Bankruptcy</p>
<input data-test="bankruptcy" type="text" className="form-control" value={getAccountField(acc => acc.bankruptcy) || "No"} disabled={true} />
</div>
<div className="col-sm">
<p>First Delinquent</p>
<input data-test="first-delinquent" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.dateFirstDelinquent))} disabled={true} />
<p>Balance</p>
<input data-test="balance" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.balance))} disabled={true} />
<p>Branch</p>
<input data-test="branch" type="text" className="form-control" value={getBranchName()} disabled={true} />
<p>Temenos Account</p>
<input data-test="reference-id" type="text" className="form-control" value={getAccountField(acc => acc.referenceId)} disabled={true}/>
</div>
<div className="col-sm">
<p>Written Off</p>
<input data-test="written-off" type="text" className="form-control" value={getWrittenOffText()} disabled={true} />
<p>Last Payment</p>
<input data-test="last-payment" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.lastPaymentDate))} disabled={true} />
<p>Current Delq Amount</p>
<input data-test="current-delq-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.delinquentAmount))} disabled={true} />
<p>Temenos Case</p>
<input data-test="reference-case-id" type="text" className="form-control" value={getAccountField(acc => acc.referenceCaseId)} disabled={true}/>
</div>
<div className="col-sm">
<p>Other Repos</p>
<input data-test="other-repos" type="text" className="form-control" value={props.otherRepos} disabled={true} />
<p>Days In Status</p>
<input data-test="days-in-status" type="text" className="form-control" value={RepoAccountService.GetDaysInStatus(props.selectedAccount)} disabled={true} />
<p>Last Pmt Amount</p>
<input data-test="last-pmt-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.lstPmtAmt))} disabled={true} />
<p>Account Last Processed</p>
<input data-test="account-last-proccessed" type="text" className="form-control" value={DateFormatting.FormatMMDDYYYHHMMA(getAccountField(acc => acc.accountLastProcessed ))} disabled={true} />
</div>
<div className="col-sm">
<p>Days Delq When Added</p>
<input data-test="days-delq-when-added" type="text" className="form-control" value={getControlledAccountField(acc => acc.daysDelinquent)} disabled={true} />
<p>Days Current Delq</p>
<input data-test="days-current-delq" type="text" className="form-control" value={getDaysCurrentDelq()} disabled={true} />
<p>Required Amount</p>
<input data-test="required-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.requiredPaymentAmount))} disabled={true} />
<p>60+ User Assigned</p>
<input data-test="sixty-plus-collector" type="text" className="form-control" value={getSixtyPlusCollector()} disabled={true} />
</div>
</div>
</div>
);
};
export default AccountDetails;
@@ -0,0 +1,65 @@
import React from 'react';
import {_AccountListView} from "./AccountListView";
import AsyncState from '../../shared/enumerations/AsyncState';
import { ShallowWrapper, shallow, mount } from 'enzyme';;
describe('AccountListView Container', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(
<_AccountListView
repoAccounts={{
state: AsyncState.Inactive,
data: []
}}
filteredAccountIds={{ state: AsyncState.Inactive }}
accountFilters={{}}
goToNextRepoAccountPage={() => {}}
goToPreviousRepoAccountPage={() => {}}
history={{} as any}
location={{} as any}
match={{} as any}
/>
);
});
it('Renders AccountListPagedTable', () => {
var tempWrapper = mount(
<_AccountListView
repoAccounts={{
state: AsyncState.Success,
data: []
}}
filteredAccountIds={{ state: AsyncState.Inactive }}
accountFilters={{}}
goToNextRepoAccountPage={() => {}}
goToPreviousRepoAccountPage={() => {}}
history={{} as any}
location={{} as any}
match={{} as any}
/>
);
const component = tempWrapper.find('AccountListPagedTable');
expect(component.length).toBe(1);
});
it ('Renders Non-Success Notification Bar', () => {
const nonSuccessNotificationBar = wrapper.find(`[data-test='notification-bar-non-success']`);
expect(nonSuccessNotificationBar.length).toBe(1);
});
it ('Renders "No Accounts Found" Notification', () => {
wrapper = wrapper.setProps({
repoAccounts: {
state: AsyncState.Success,
data: []
}
});
const noAccountsNotificationBar = wrapper.find(`[data-test='notification-bar-no-accounts']`);
expect(noAccountsNotificationBar.length).toBe(1);
});
});
@@ -0,0 +1,83 @@
import React from "react";
import {connect} from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { goToNextRepoAccountPage, goToPreviousRepoAccountPage } from "../../actions/RepoAccountActions";
import NotificationBar, { NotificationBarType } from "../../components/notification-bar/NotificationBar";
import { getCurrentlyFilteredAccounts } from "../../selectors/RepoAccountSelectors";
import { IRepoAccount, IRepoAccountFilters } from "../../services/service-types/RepoAccountTypes";
import AsyncState, { mapAsyncStateToNotificationType } from "../../shared/enumerations/AsyncState";
import Routes from "../../shared/Routes";
import IAsyncStateContainer, { IAsyncStateContainerStrict } from "../../shared/types/IAsyncStateContainer";
import { IPagedResponse } from "../../shared/types/IPagedResponse";
import {IAppState} from "../../store/Store";
import AccountListPagedTable from "./account-list-paged-table/AccountListPagedTable";
interface IPropsFromState {
filteredAccountIds: IAsyncStateContainer<IPagedResponse<number>>;
repoAccounts: IAsyncStateContainerStrict<IRepoAccount[]>;
accountFilters: IRepoAccountFilters;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoAccounts: getCurrentlyFilteredAccounts(state),
accountFilters: state.repoAccountState.accountFilters,
filteredAccountIds: state.repoAccountState.filteredAccounts
};
};
interface IDispatchFromState {
goToNextRepoAccountPage: () => any;
goToPreviousRepoAccountPage: () => any;
}
const mapDispatchToProps: IDispatchFromState = {
goToNextRepoAccountPage: goToNextRepoAccountPage,
goToPreviousRepoAccountPage: goToPreviousRepoAccountPage
};
type Props = IPropsFromState & RouteComponentProps & IDispatchFromState;
export const AccountListView: React.FC<Props> = (props: Props) => {
const goToAccount = (account: IRepoAccount) => {
props.history.push(Routes.Account(account.repoId));
};
return (
<div>
{
props.repoAccounts.state !== AsyncState.Success
?
<NotificationBar
type={mapAsyncStateToNotificationType(props.repoAccounts.state)}
text={props.repoAccounts.state}
data-test="notification-bar-non-success"
/>
:
<AccountListPagedTable
repoAccounts={props.repoAccounts.data}
pagedResponse={props.filteredAccountIds.data}
onRepoAccountClicked={goToAccount}
onPrevCallback={props.goToPreviousRepoAccountPage}
onNextCallback={props.goToNextRepoAccountPage}
/>
}
{props.repoAccounts.state === AsyncState.Success && props.repoAccounts.data.length === 0 ?
<NotificationBar
type={NotificationBarType.Warning}
text="No Accounts Found"
data-test="notification-bar-no-accounts"
/>
: null}
</div>
);
};
export const _AccountListView = AccountListView;
// This should be ideally done with componse.
// However, the type wasn't being properly inferred.
// This may be something worth looking into at a later point
export default withRouter(connect<IPropsFromState, IDispatchFromState, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AccountListView));
@@ -0,0 +1,38 @@
import React from "react";
import TablePagination from "../../../components/table-pagination/TablePagination";
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import { IPagedResponse } from "../../../shared/types/IPagedResponse";
import AccountListTable from "../account-list-table/AccountListTable";
interface IAccountListPagedTableProps {
repoAccounts: IRepoAccount[];
pagedResponse?: IPagedResponse<any>;
onRepoAccountClicked: (repoAccount: IRepoAccount) => any;
onPrevCallback: () => any;
onNextCallback: () => any;
}
const AccountListPagedTable: React.FC<IAccountListPagedTableProps> = (props) => {
return (
<div className="account-list-paged-table">
<div className="account-table-wrapper">
<AccountListTable
repoAccounts={props.repoAccounts}
onRepoAccountClicked={props.onRepoAccountClicked}
/>
</div>
<TablePagination
totalItems={props.pagedResponse ? props.pagedResponse.totalItems : 0}
itemsPerPage={props.pagedResponse ? props.pagedResponse.items.length : 0}
currentPage={props.pagedResponse ? props.pagedResponse.currentPage : 1}
totalPages={props.pagedResponse ? props.pagedResponse.totalPages : 1}
isPrevDisabled={props.pagedResponse ? !props.pagedResponse.previousPage : false}
isNextDisabled={props.pagedResponse ? !props.pagedResponse.nextPage : false}
prevPageCallback={props.onPrevCallback}
nextPageCallback={props.onNextCallback}
/>
</div>
);
};
export default AccountListPagedTable;
@@ -0,0 +1,37 @@
.account-list-table {
.main-panel {
padding: 75px 15px;
min-height: calc(101vh - 80px);
}
table.lcu {
display: flex;
flex-flow: column;
width: 100%;
}
thead.lcu {
flex: 0 0 auto;
}
tbody.lcu {
flex: 1 1 auto;
display: block;
overflow-y: auto;
overflow-x: hidden;
}
tr.lcu {
width: 100%;
display: table;
table-layout: fixed;
}
tr, tr * {
user-select: none;
}
.account-no-label {
margin-bottom: 0;
}
}
@@ -0,0 +1,65 @@
import React from "react";
import CollateralTextSummary from "../../../components/collateral/text-summary/CollateralTextSummary";
import RepoAccountService from "../../../services/RepoAccountService";
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
import MonetaryFormatting from "../../../shared/formatting/MonetaryFormatting";
import StringFormatting from "../../../shared/formatting/StringFormatting";
import "./AccountListTable.scss";
interface IAccountListTableProps {
repoAccounts: IRepoAccount[];
onRepoAccountClicked: (repoAccount: IRepoAccount) => any;
}
export const AccountListTable: React.FC<IAccountListTableProps> = (props) => {
return (
<table className="account-list-table table table-striped table-hover">
<thead className="thead-dark">
<tr>
<th scope="col">AccountNo</th>
<th scope="col">Borrower</th>
<th scope="col">Collateral</th>
<th scope="col">LoanBalance</th>
<th scope="col">Status</th>
<th scope="col">Days In Status</th>
</tr>
</thead>
<tbody>
{props.repoAccounts.map((repoAccount, _) => {
return (
<tr key={repoAccount.repoId} onClick={() => props.onRepoAccountClicked(repoAccount)}>
<td>
<p className="account-no-label">{repoAccount.accountNo}</p>
{RepoAccountService.IsAccountClosed(repoAccount) ?
<p className="text-danger"><b>CLOSED</b></p>
: null}
</td>
<td>{
repoAccount.borrowers.map((borrower) => {
return (
<p key={borrower.individualId}>{StringFormatting.ToTitleCase(borrower.individualName)}</p>
);
})
}</td>
<td>{
repoAccount.collaterals.map((collateral) => {
return (
<p key={collateral.collateralId}>
<CollateralTextSummary collateral={collateral}/>
</p>
);
})
}</td>
<td><p>{MonetaryFormatting.FormatUSD(repoAccount.balance)}</p></td>
<td><p>{repoAccount.status}</p></td>
<td><p>{RepoAccountService.GetDaysInStatus(repoAccount)}</p></td>
</tr>
);
})}
</tbody>
</table>
);
};
export default AccountListTable;
@@ -0,0 +1,49 @@
.account-details-holder p {
margin-bottom: 0;
}
.account-details-holder input {
margin-bottom: 1em;
}
.tab-view-wrapper {
margin-top: 2em;
}
.account-page-input-section p, .account-page-input-section input {
font-size: .9em;
}
.account-page-input-section input {
height: 2em;
}
.back-navigation-header .fa-chevron-left {
cursor: pointer;
margin-right: .5em;
}
.back-navigation-header * {
text-align: center;
margin-bottom: 0;
}
.back-navigation-header .row .col-sm:first-child * {
text-align: left;
}
.back-navigation-header .row .col-sm:last-child * {
text-align: right;
}
@media (max-width: 575px) {
.back-navigation-header * {
text-align: left;
}
.back-navigation-header .row .col-sm:last-child * {
text-align: left;
}
.account-page-container .col-sm:not(:first-child) {
padding-top: 1em;
}
}
@@ -0,0 +1,94 @@
import { shallow, ShallowWrapper } from "enzyme";
import moment from "moment";
import React from "react";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import { _AccountPage } from "./AccountPage";
{/* - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
describe("AccountPage component", () => {
let wrapper: ShallowWrapper;
const dummy = {} as any;
const matchParams = {
params: {
accountno: "123456",
},
} as any;
const selectedAccount: IRepoAccount = {
repoId: 476,
accountNo: "123456",
status: "string",
memberNo: 1453,
loanNo: 0,
accountTypeCode: "acct-type-code",
branch: 0,
balance: 0,
dueToPayDate: moment("1941-12-07"),
o4RDate: moment("1945-05-07"),
dateFirstDelinquent: moment("1918-11-11"),
delinquentAmount: 0,
daysDelinquent: 101,
paidSkipAmount: 0,
subStatus: "string",
requiredPaymentAmount: 3.14,
currentDelinquentAmount: 19.45,
lstPmtAmt: 1912.00000,
origAmount: 42,
lastPaymentDate: moment("1865-04-15"),
dateFirstIntellect: moment("1963-11-22"),
primaryIndividualId: "string",
manualRedemptionDate: moment("1812-06-12"),
borrowers: [{
repoId: 1,
individualId: "1",
ownershipType: 6,
individualName: "John Doe",
}],
collaterals: [],
repoStatusChanges: [],
}
beforeEach(() => {
wrapper = shallow(
<_AccountPage
repoAccounts={[selectedAccount]}
allowedStatusChanges={[]}
match={matchParams}
history={dummy}
location={dummy}
putRepoAccount={dummy}
refreshCollateral={() => {}}
selectedAccount={{
state: AsyncState.Inactive
}}
filteredAccounts={{
state: AsyncState.Inactive
}}
fetchSelectedRepoAccount={() => {}}
searchRepoAccounts={() => {}}
getInitialRepoAccounts={() => {}}
/>
);
});
it ("Renders Account Header", () => {
const component = wrapper.find(`[data-test='account-page-header']`);
expect(component.length).toBe(1);
});
it ("Renders Account Details", () => {
const accountDetails = wrapper.find(`[data-test='account-page-details']`);
expect(accountDetails.length).toBe(1);
});
it ("Renders the Tab View", () => {
const tabView = wrapper.find(`[data-test='account-page-tab-view']`);
expect(tabView.length).toBe(1);
});
});
*/}
@@ -0,0 +1,182 @@
import Axios from "axios";
import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps } from "react-router";
import { toast } from "react-toastify";
import { compose } from "redux";
import { fetchSelectedRepoAccount, putRepoAccount, refreshCollateral } from "../../actions/RepoAccountActions";
import AccountPageHeader from "../../components/account-page-header/AccountPageHeader";
import { IAllowedStatusChange, IBranchSetEntry } from "../../reducers/SettingsReducer";
import { getSelectedAccount } from "../../selectors/RepoAccountSelectors";
import { getBranchMapping } from "../../selectors/SettingsSelector";
import { ICollateral, IRepoAccount, IRepoAccountMap } from "../../services/service-types/RepoAccountTypes";
import Configuration from "../../shared/configuration/Configuration";
import AsyncState from "../../shared/enumerations/AsyncState";
import AxiosMessageFormatting from "../../shared/formatting/AxiosMessageFormatting";
import UrlFormatting from "../../shared/formatting/UrlFormatting";
import ToastDefaults from "../../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import { IAppState } from "../../store/Store";
import AccountDetails from "../account-details/AccountDetails";
import AccountPageTabView from "./account-page-tab-view/AccountPageTabView";
import "./AccountPage.css";
const cloneDeep = require("lodash.clonedeep");
export interface IAccountPageRouterProps {
readonly repoId: string;
}
interface IPropsFromState {
readonly repoAccounts: IRepoAccountMap;
readonly allowedStatusChanges: IAllowedStatusChange[];
readonly selectedAccount: IAsyncStateContainer<IRepoAccount>;
readonly branchMap: IBranchSetEntry[];
}
interface IPropsFromDispatch {
readonly refreshCollateral: (collateral: ICollateral, memberNo: number, loanNo: number) => any;
readonly putRepoAccount: (repoAccount: IRepoAccount) => any;
readonly fetchSelectedRepoAccount: (repoId: number) => any;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoAccounts: state.repoAccountState.repoAccounts,
allowedStatusChanges: state.settingsState.allowedStatusChanges,
selectedAccount: getSelectedAccount(state),
branchMap: getBranchMapping(state)
};
};
const mapDispatchToProps: IPropsFromDispatch = {
fetchSelectedRepoAccount: fetchSelectedRepoAccount,
refreshCollateral: refreshCollateral,
putRepoAccount: putRepoAccount
};
interface IAccountPageState {
numberOfOtherRepos: IAsyncStateContainer<number>;
}
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps<IAccountPageRouterProps>;
class AccountPage extends React.Component<Props, IAccountPageState> {
constructor(props: Props) {
super(props);
this.state = {
numberOfOtherRepos: { state: AsyncState.Loading }
};
this.returnToHomePage = this.returnToHomePage.bind(this);
this.saveRepoAccountStatus = this.saveRepoAccountStatus.bind(this);
}
componentWillMount() {
this.props.fetchSelectedRepoAccount(parseInt(this.props.match.params.repoId));
}
componentDidUpdate(prevProps: Props) {
if (
prevProps.selectedAccount.state !== this.props.selectedAccount.state &&
this.props.selectedAccount.state === AsyncState.Success &&
this.props.selectedAccount.data
) {
this.loadOtherRepos(this.props.selectedAccount.data.memberNo);
}
}
async loadOtherRepos(memberNo: number) {
try {
// This could possibly return false positives. At some point, a new endpoint should be created
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_search),
{ key: "searchPhrase", value: memberNo }
);
const result = await Axios.get(parameterizedUrl);
const historyResult = result.data as IRepoAccount[];
this.setState({
numberOfOtherRepos: {
data: historyResult.length - 1,
state: AsyncState.Success,
},
});
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
this.setState({
numberOfOtherRepos: {
state: AsyncState.Failure,
},
});
}
}
saveRepoAccountStatus(newStatus: string, selectedRepoAccount?: IRepoAccount): void {
if (selectedRepoAccount) {
const copiedRepoAccount = cloneDeep(selectedRepoAccount) as IRepoAccount;
copiedRepoAccount.status = newStatus;
this.props.putRepoAccount(copiedRepoAccount);
}
}
returnToHomePage() {
this.props.history.goBack();
}
getAccountNoHeader(selectedAccount: IAsyncStateContainer<IRepoAccount>): string {
if (selectedAccount.state === AsyncState.Success) {
return selectedAccount.data ? selectedAccount.data.accountNo : "-";
}
return selectedAccount.state;
}
getOtherRepos(): string {
if (this.state.numberOfOtherRepos.state === AsyncState.Success && this.state.numberOfOtherRepos.data !== undefined) {
return (this.state.numberOfOtherRepos.data).toString();
} else {
return this.state.numberOfOtherRepos.state;
}
}
render() {
return (
<div className="account-page-container container-fluid main-panel" data-test="mainPanel">
<AccountPageHeader
accountNoHeader={this.getAccountNoHeader(this.props.selectedAccount)}
selectedRepoAccount={this.props.selectedAccount.data}
allowedStatusChanges={this.props.allowedStatusChanges}
onBackButtonClicked={this.returnToHomePage}
onStatusChange={(newStatus) => this.saveRepoAccountStatus(newStatus, this.props.selectedAccount.data)}
putRepoAccount={this.props.putRepoAccount}
data-test="account-page-header"
/>
<AccountDetails
selectedAccount={this.props.selectedAccount.data}
otherRepos={this.getOtherRepos()}
refreshCollateral={this.props.refreshCollateral}
branchMapping={this.props.branchMap}
data-test="account-page-details"
/>
<div className="tab-view-wrapper">
<AccountPageTabView
selectedAccount={this.props.selectedAccount.data}
data-test="account-page-tab-view"
/>
</div>
</div>
);
}
}
// https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability
// Use a function composition utility: compose(f, g, h) is the same as (...args) => f(g(h(...args)))
const enhance = compose(
// connect<ReduxInjected, DispatchInjected, GivenByParentWhenUsingThisComponent, ReduxStoreType>
connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)
);
const EnhancedComponent = enhance(AccountPage);
export const _AccountPage = AccountPage; // Export the non-connected version for ease in unit testing
export default EnhancedComponent; // connect(mapStateToProps)(AccountListView);
@@ -0,0 +1,26 @@
import { shallow } from "enzyme";
import React from "react";
import AsyncState from "../../../shared/enumerations/AsyncState";
import { _AccountPageTabView } from "./AccountPageTabView";
{/* - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
describe("AccountPageTabView Container", () => {
it ("Only Renders Partial Tab if SelectedAccount is Undefined", () => {
const wrapper = shallow(
<_AccountPageTabView
repoCompanies={{ state: AsyncState.Inactive}}
putCollateral={() => {}}
markRetitled={() => {}}
externalUserMap={[]}
/>
);
const emptyTabs = wrapper.find(`[data-test='account-page-tab-view-empty-tabs']`);
const loadedTabs = wrapper.find(`[data-test='account-page-tab-view-loaded-tabs']`);
expect(emptyTabs.length).toBe(1);
expect(loadedTabs.length).toBe(0);
});
});
*/}
@@ -0,0 +1,333 @@
/* eslint-disable eqeqeq */
import Axios from "axios";
import moment from "moment/moment";
import React from "react";
import {connect} from "react-redux";
import {toast} from "react-toastify";
import TabView from "../../../components/tab-view/TabView";
import {IRepoCompany} from "../../../reducers/RepoCompanyReducer";
import {IExternalUser} from "../../../reducers/SettingsReducer";
import {HistorySource, ITemenosHistory} from "../../../services/service-types/AccountHistoryTypes";
import {IRepoAccount} from "../../../services/service-types/RepoAccountTypes";
import Configuration from "../../../shared/configuration/Configuration";
import DateConversions from "../../../shared/conversions/DateConversions";
import AsyncState from "../../../shared/enumerations/AsyncState";
import AxiosMessageFormatting from "../../../shared/formatting/AxiosMessageFormatting";
import UrlFormatting from "../../../shared/formatting/UrlFormatting";
import ToastDefaults from "../../../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../../store/Store";
import AccountActivity from "../../account-activity/AccountActivity";
import AuctionTab from "../../auction-tab/AuctionTab";
import RefundStatusTab from "../../refund-status-tab/RefundStatusTab";
import RepoHistoryTab from "../../repo-history-tab/RepoHistoryTab";
import RepoInfoTab from "../../repo-info-tab/RepoInfoTab";
import RepoStatusChangeHistoryTable from "../../repo-status-change-history-table/RepoStatusChangeHistoryTable";
import TransactionHistoryTable, {ITransactionHistory} from "../../transaction-history-table/TransactionHistoryTable";
import {getExternalUserMap, getRepoTypeMap} from "./../../../selectors/SettingsSelector";
interface IAccountPageTabViewProps {
selectedAccount?: IRepoAccount;
}
interface IPropsFromState {
readonly repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
readonly externalUserMap: IExternalUser[];
readonly repoTypeMap: string[];
readonly transactionTypeCodes: string[];
readonly transactionTypeCodeQueryPrefix: string;
readonly transactionHistoryParameterDateFormat: string;
readonly transactionHistoryStartDateOffset: number;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
repoCompanies: state.repoCompanyState.repoCompanies,
externalUserMap: getExternalUserMap(state),
repoTypeMap: getRepoTypeMap(state),
transactionTypeCodes: state.settingsState.transactionTypeCodes,
transactionTypeCodeQueryPrefix: state.settingsState.transactionTypeCodeQueryPrefix,
transactionHistoryParameterDateFormat: state.settingsState.transactionHistoryParameterDateFormat,
transactionHistoryStartDateOffset: state.settingsState.transactionHistoryStartDateOffset
};
};
interface IAccountPageState {
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
transactionHistories: IAsyncStateContainer<ITransactionHistory[]>;
}
type Props = IAccountPageTabViewProps & IPropsFromState;
class AccountPageTabView extends React.Component<Props, IAccountPageState> {
constructor(props: Props) {
super(props);
this.state = {
caseHistories: {state: AsyncState.Inactive},
accountHistories: {state: AsyncState.Inactive},
transactionHistories: {state: AsyncState.Inactive},
};
this.updateCaseHistory = this.updateCaseHistory.bind(this);
this.updateAccountHistory = this.updateAccountHistory.bind(this);
this.updateTransactionHistory = this.updateTransactionHistory.bind(this);
this.updateHistories = this.updateHistories.bind(this);
}
componentDidUpdate(prevProps: Props) {
// Update letters if the Selected Account has updated
if (this.props.selectedAccount !== prevProps.selectedAccount) {
this.updateHistories();
}
}
async updateCaseHistory() {
if (!this.props.selectedAccount) {
return;
}
if (
this.state.caseHistories.state === AsyncState.Inactive ||
this.state.caseHistories.state === AsyncState.Failure
) {
this.setState({
caseHistories: {
state: AsyncState.Loading,
},
});
if (this.props.selectedAccount.referenceCaseId) {
const caseHistories = await this.getCaseHistory(this.props.selectedAccount.referenceCaseId);
this.setState({
caseHistories: caseHistories,
});
} else {
this.setState({
caseHistories: {
state: AsyncState.Failure
}
});
}
}
}
async updateAccountHistory() {
if (!this.props.selectedAccount) {
return;
}
if (
this.state.accountHistories.state === AsyncState.Inactive ||
this.state.accountHistories.state === AsyncState.Failure
) {
this.setState({
accountHistories: {
state: AsyncState.Loading,
},
});
if (this.props.selectedAccount.referenceId) {
const accountHistories = await this.getAccountHistory(this.props.selectedAccount.referenceId);
this.setState({
accountHistories: accountHistories,
});
} else {
this.setState({
accountHistories: {
state: AsyncState.Failure
}
});
}
}
}
async updateTransactionHistory() {
if (!this.props.selectedAccount) {
return;
}
if (
this.state.transactionHistories.state === AsyncState.Inactive ||
this.state.transactionHistories.state === AsyncState.Failure
) {
this.setState({
transactionHistories: {
state: AsyncState.Loading,
},
});
const transactionHistories = await this.getTransactionHistory(this.props.selectedAccount.accountNo);
this.setState({
transactionHistories: transactionHistories,
});
}
}
async getCaseHistory(caseId: string): Promise<IAsyncStateContainer<ITemenosHistory[]>> {
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.case_history),
{key: "caseId", value: caseId},
);
try {
const result = await Axios.get(parameterizedUrl);
const historyResult = result.data as ITemenosHistory[];
historyResult.map((x) => {
x.Created = DateConversions.utcToMoment(x.Created);
x.Source = HistorySource.Case;
return x;
});
return {
data: historyResult,
state: AsyncState.Success,
};
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure,
};
}
}
async getAccountHistory(accountNo: string): Promise<IAsyncStateContainer<ITemenosHistory[]>> {
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.account_history),
{key: "accountNo", value: accountNo},
);
try {
const result = await Axios.get(parameterizedUrl);
const historyResult = result.data as ITemenosHistory[];
historyResult.map((x) => {
x.Created = DateConversions.utcToMoment(x.Created);
x.Source = HistorySource.Account;
return x;
});
return {
data: historyResult,
state: AsyncState.Success,
};
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure,
};
}
}
async getTransactionHistory(accountNo: string): Promise<IAsyncStateContainer<ITransactionHistory[]>> {
const parameterizedUrl = this.getTransactionHistoriesURL(accountNo);
if (!parameterizedUrl) {
return {
state: AsyncState.Failure,
};
}
try {
const result = await Axios.get(parameterizedUrl);
if (result.data.length < 1)
{
return result.data;
}
const transactionHistoryResult = result.data as ITransactionHistory[];
transactionHistoryResult.map((x) => {
x.postingDate = DateConversions.utcToMoment(x.postingDate);
x.effectiveDate = DateConversions.utcToMoment(x.effectiveDate);
return x;
});
return {
data: transactionHistoryResult,
state: AsyncState.Success,
};
} catch (err) {
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure,
};
}
}
getTransactionHistoriesURL(accountNo: string): string | undefined {
return UrlFormatting.ReplaceURLParams(
Configuration.PrefixCoreUrl(Configuration.Get.urls.core.loan_transactions),
{key: "loanNo", value: accountNo},
{key: "startDate", value: moment().add(this.props.transactionHistoryStartDateOffset, "d").format(this.props.transactionHistoryParameterDateFormat)},
{key: "endDate", value: moment().format(this.props.transactionHistoryParameterDateFormat)}
) + UrlFormatting.FormatArrayValuesIntoQuery(this.props.transactionTypeCodes, this.props.transactionTypeCodeQueryPrefix);
}
updateHistories() {
this.updateCaseHistory();
this.updateAccountHistory();
}
render() {
if (!this.props.selectedAccount) {
return (
<div data-test="account-page-tab-view-empty-tabs">
<TabView
pages={[
{
header: "Activity",
element: (<div/>),
},
]}
/>
</div>
);
} else {
return (
<div data-test="account-page-tab-view-loaded-tabs">
<TabView
pages={[
{
header: "Activity",
element: <AccountActivity selectedAccount={this.props.selectedAccount}
caseHistories={this.state.caseHistories}
accountHistories={this.state.accountHistories}
externalUserMap={this.props.externalUserMap}/>,
tabOpenCallback: this.updateHistories,
},
{
header: "Repo Info",
element: <RepoInfoTab selectedAccount={this.props.selectedAccount}
repoCompanies={this.props.repoCompanies.data}
repoTypeMap={this.props.repoTypeMap}/>,
},
{
header: "Auction Info",
element: <AuctionTab selectedAccount={this.props.selectedAccount}
caseHistories={this.state.caseHistories}
accountHistories={this.state.accountHistories}/>,
tabOpenCallback: this.updateHistories,
},
{
header: "Repo History",
element: <RepoHistoryTab accountNo={this.props.selectedAccount.accountNo}
accountHistories={this.state.accountHistories}
caseHistories={this.state.caseHistories}
externalUserMap={this.props.externalUserMap}/>,
tabOpenCallback: this.updateHistories
},
{
header: "Transaction History",
element: <TransactionHistoryTable accountNo={this.props.selectedAccount.accountNo}
transactionHistories={this.state.transactionHistories}/>,
tabOpenCallback: this.updateTransactionHistory
},
{
header: "Repo Status Changes",
element: <RepoStatusChangeHistoryTable accountNo={this.props.selectedAccount.accountNo}
repoStatusChanges={this.props.selectedAccount.repoStatusChanges}/>,
},
{
header: "Refund Status",
element: <RefundStatusTab selectedAccount={this.props.selectedAccount}/>
}
]}
/>
</div>
);
}
}
}
export const _AccountPageTabView = AccountPageTabView;
export default connect<IPropsFromState, null, {}, IAppState>(mapStateToProps, null)(AccountPageTabView);
@@ -0,0 +1,216 @@
import {shallow, ShallowWrapper} from "enzyme";
import { createLocation, createMemoryHistory } from "history";
import moment from "moment";
import React from "react";
import { match } from "react-router";
import {ICollectionsAccount } from "../../reducers/CollectionsAccountReducer";
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {_AdminAccountLookup} from "./AdminAccountLookup";
const searchPhrase = "123-4567";
const singleCollectionsAccount : IAsyncStateContainer<ICollectionsAccount[]> = {
data: [{
accountId : 555555,
accountIdentifier: "5",
accountNumber: "123-4567",
accountStatus: "status",
accountTypeId: 5,
activeFlag: true,
bankruptcyFlag: true,
branch: "branch",
dueDate: moment("2020-12-5"),
firstPaymentDueDate: moment("2020-11-5"),
originalLoanDate: moment("1995-12-3"),
dqAmount: 5,
cases: [{
caseId: 1,
caseNumber: "123",
caseDescription: "This is a test",
caseSTageId: 5,
repoStatus: 2,
caseStatusId: 3,
caseTitle: "Test",
caseTypeBasisId: 4,
caseTypeId: 6,
createDate: moment("2020-10-5"),
createdUserId: 12345,
}]
}],
state: AsyncState.Success
};
const associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]> = {
data : [ {repoId: 5,
accountNo: "4567",
status: "04R",
memberNo: 123456,
loanNo: 164,
accountTypeCode: "typecode",
branch: 12,
balance: 100.00,
delinquentAmount: 150,
daysDelinquent: 2,
requiredPaymentAmount: 105.00,
origAmount: 1,
primaryIndividualId: "indvid",
borrowers: [],
collaterals: [],
repoStatusChanges: [],
origAccountTypeCode: "test"
}],
state: AsyncState.Success
};
const didAddToQueue: IAsyncStateContainer<boolean> = {
data: false,
state: AsyncState.Inactive
};
const history = createMemoryHistory();
const path = `/route/:id`;
const routematch: match<{ id: string }> = {
isExact: false,
path,
url: path.replace(":id", "1"),
params: { id: "1" }
};
const location = createLocation(routematch.url);
describe("AdminAccountLookup Container OK", () => {
let wrapper: ShallowWrapper;
const combinedStates =
beforeEach(() => {
wrapper = shallow(<_AdminAccountLookup
didAddToQueue={didAddToQueue}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>
);
});
it("Renders The Main Container", () => {
const component = wrapper.find(`[data-test='account-lookup-main']`);
expect(component.length).toBe(1);
});
it("Renders AdminAccountLookupDetails", () =>{
const component = wrapper.find(`[data-test='collections-details']`);
expect(component.length).toBe(1);
});
it("Renders Button For Pushing to Repo Account", () => {
const component = wrapper.find(`[data-test='put-to-queue-button-found']`);
expect(component.length).toBe(1);
});
it("Renders Put Notification Success",() => {
const tempDidAddToQueue = didAddToQueue;
tempDidAddToQueue.data = true;
tempDidAddToQueue.state = AsyncState.Success;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={tempDidAddToQueue}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>
);
const notification = tempWrapper.find(`[data-test='add-to-queue-success']`);
expect(notification.length).toBe(1);
const button = tempWrapper.find(`[data-test='put-to-queue-button-found']`);
expect(button.length).toBe(1);
});
it("Does Not Render Active Put Button When Collections Is Not Found",() => {
const noCollectionsAccount = singleCollectionsAccount;
noCollectionsAccount.data = [];
noCollectionsAccount.state = AsyncState.Failure;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={didAddToQueue}
collectionsAccount={noCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const button = tempWrapper.find(`[data-test='put-to-queue-button-not-found']`);
expect(button.length).toBe(1);
});
it("Renders Loading Put To Queue", () => {
const loadingPut = didAddToQueue;
loadingPut.data = true;
loadingPut.state = AsyncState.Loading;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={loadingPut}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const notification = tempWrapper.find(`[data-test='add-to-queue-loading']`);
expect(notification.length).toBe(1);
});
it("Renders Success Put To Queue", () => {
const loadingPut = didAddToQueue;
loadingPut.data = true;
loadingPut.state = AsyncState.Success;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={loadingPut}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const notification = tempWrapper.find(`[data-test='add-to-queue-success']`);
expect(notification.length).toBe(1);
});
it("Renders Failed Put To Queue", () => {
const loadingPut = didAddToQueue;
loadingPut.data = true;
loadingPut.state = AsyncState.Failure;
const tempWrapper = shallow(<_AdminAccountLookup
didAddToQueue={loadingPut}
collectionsAccount={singleCollectionsAccount}
searchPhrase={searchPhrase}
getCollectionsAccount={()=>{}}
setSearchPhraseCollectionsAccounts={()=>{}}
putAccountOnRepoQueue={()=>{}}
history={history}
location={location}
match={routematch}
/>);
const notification = tempWrapper.find(`[data-test='add-to-queue-error']`);
expect(notification.length).toBe(1);
});
});
@@ -0,0 +1,326 @@
import {
getCollectionsAccount,
putAccountOnRepoQueue,
setSearchPhraseCollectionsAccounts
} from "../../actions/CollectionsAccountActions";
import NotificationBar, {NotificationBarType} from "../../components/notification-bar/NotificationBar";
import Axios from "axios";
import React from "react";
import {connect} from "react-redux";
import {RouteComponentProps} from "react-router";
import {toast} from "react-toastify";
import TabView from "../../components/tab-view/TabView";
import {ICollectionsAccount} from "../../reducers/CollectionsAccountReducer";
import {IRepoAccount} from "../../services/service-types/RepoAccountTypes";
import Configuration from "../../shared/configuration/Configuration";
import AsyncState from "../../shared/enumerations/AsyncState";
import StringFormatting from "../../shared/formatting/StringFormatting";
import Routes from "../../shared/Routes";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {IAppState} from "../../store/Store";
import ProcessingReceiptService from "./../../services/ProccessingReceiptService";
import {IProcessingReceipt} from "./../../services/service-types/ProcessingReceiptTypes";
import DateConversions from "./../../shared/conversions/DateConversions";
import AxiosMessageFormatting from "./../../shared/formatting/AxiosMessageFormatting";
import UrlFormatting from "./../../shared/formatting/UrlFormatting";
import ToastDefaults from "./../../shared/notifications/ToastDefaults";
import {ProcessingReceiptTable} from "./../processing-receipt-table/processingReceiptTable";
import AdminAccountLookupDetails from "./admin-account-lookup-details/AdminAccountLookupDetails";
import {AdminAccountLookupReposTab} from "./admin-account-lookup-repos-tab/AdminAccountLookupReposTab";
interface IPropsFromState {
searchPhrase: string;
collectionsAccount: IAsyncStateContainer<ICollectionsAccount[]>;
didAddToQueue: IAsyncStateContainer<boolean>;
}
const mapStateToProps = (state: IAppState): IPropsFromState => {
return {
collectionsAccount: state.collectionsAccountState.collectionsAccount,
searchPhrase: state.collectionsAccountState.searchPhrase,
didAddToQueue: state.collectionsAccountState.didAddToQueue,
};
};
interface IPropsFromDispatch {
getCollectionsAccount: (accountNo: string) => any;
setSearchPhraseCollectionsAccounts: (searchPhrase: string) => any;
putAccountOnRepoQueue: (collectionsAccount: ICollectionsAccount) => any;
}
const mapDispatchToProps: IPropsFromDispatch = {
getCollectionsAccount: getCollectionsAccount,
setSearchPhraseCollectionsAccounts: setSearchPhraseCollectionsAccounts,
putAccountOnRepoQueue: putAccountOnRepoQueue
};
interface IAdminAccountLookupState {
processingReceipts: IAsyncStateContainer<IProcessingReceipt[]>;
associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]>;
}
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps;
class AdminAccountLookup extends React.Component <Props, IAdminAccountLookupState> {
constructor(props: any) {
super(props);
this.state = {
processingReceipts: {
state: AsyncState.Inactive
},
associatedRepoAccounts: {
state: AsyncState.Inactive
}
};
this.handleSearchChange = this.handleSearchChange.bind(this);
this.searchAccounts = this.searchAccounts.bind(this);
this.handleSearchKeyDown = this.handleSearchKeyDown.bind(this);
this.goToAccount = this.goToAccount.bind(this);
this.pushToQueue = this.pushToQueue.bind(this);
this.updateProcessingReceipts = this.updateProcessingReceipts.bind(this);
this.getProcessingReceipts = this.getProcessingReceipts.bind(this);
this.getAssociatedRepoAccounts = this.getAssociatedRepoAccounts.bind(this);
this.updateProcessingReceipts = this.updateProcessingReceipts.bind(this);
}
goToAccount = (account: IRepoAccount) => {
this.props.history.push(Routes.Account(account.repoId));
};
pushToQueue = () => {
if (this.props.collectionsAccount && this.props.collectionsAccount.data && this.props.collectionsAccount.data.length === 1) {
this.props.putAccountOnRepoQueue(this.props.collectionsAccount.data[0]);
}
};
async searchAccounts() {
if (this.props.searchPhrase.trim() !== "") {
await this.props.getCollectionsAccount(this.props.searchPhrase);
await this.updateAssociatedRepoAccounts();
await this.updateProcessingReceipts();
} else {
this.props.collectionsAccount.state = AsyncState.Inactive;
this.state.associatedRepoAccounts.state = AsyncState.Inactive;
}
}
async handleSearchKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.keyCode === 13) { // Enter
await this.searchAccounts();
}
}
async getAssociatedRepoAccounts(accountNo: string): Promise<IAsyncStateContainer<IRepoAccount[]>> {
const paramterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single_by_account_no), {
key: "accountNo",
value: accountNo
}
);
try {
const results = await Axios.get(paramterizedUrl);
const repoAccounts = results.data as IRepoAccount[];
return {
data: repoAccounts,
state: AsyncState.Success
};
} catch (err) {
if (err.response.status === 404) {
return {
state: AsyncState.Success,
data: []
};
}
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure
};
}
}
async getProcessingReceipts(accountNo: string): Promise<IAsyncStateContainer<IProcessingReceipt[]>> {
const paramterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.account_processing_receipts), {
key: "accountNo",
value: accountNo
}
);
try {
const results = await Axios.get(paramterizedUrl);
const processingReceiptResults = results.data as IProcessingReceipt[];
processingReceiptResults.map(x => {
x.processed = DateConversions.utcToMoment(x.processed);
return x;
});
return {
data: ProcessingReceiptService.sortByProcessedDate(processingReceiptResults),
state: AsyncState.Success
};
} catch (err) {
if (err.response.status === 404) {
return {
state: AsyncState.Success,
data: []
};
}
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
window.console.error(err);
return {
state: AsyncState.Failure
};
}
}
async updateProcessingReceipts() {
this.setState({
processingReceipts: {
state: AsyncState.Loading
}
});
const processingReceipts = await this.getProcessingReceipts(this.props.searchPhrase);
this.setState({
processingReceipts: processingReceipts
});
}
async updateAssociatedRepoAccounts() {
this.setState({
associatedRepoAccounts: {
state: AsyncState.Loading
}
});
const associatedRepoAccounts = await this.getAssociatedRepoAccounts(this.props.searchPhrase);
this.setState({
associatedRepoAccounts: associatedRepoAccounts
});
}
handleSearchChange(event: React.ChangeEvent<HTMLInputElement>) {
this.props.setSearchPhraseCollectionsAccounts(StringFormatting.TrimLeft(event.target.value));
}
render() {
return (
<div className="container-fluid main-panel" data-test="account-lookup-main">
{createPutToQueueNotifications(this.props.didAddToQueue.state)}
<div>
<h2>Account Lookup</h2>
<div className="form-inline d-flex">
<input
data-test="text-input"
className="form-control mr-sm-2 mb-3"
type="search"
aria-label="Search"
placeholder="Account No"
value={this.props.searchPhrase}
onChange={this.handleSearchChange}
onKeyDown={this.handleSearchKeyDown}
/>
<button
className="btn btn-primary mb-3"
type="submit"
onClick={this.searchAccounts}
>
Search
</button>
{createPushToRepoButton(this.props.collectionsAccount.state, this.pushToQueue, this.props.didAddToQueue.data)}
</div>
<hr/>
<AdminAccountLookupDetails data-test="collections-details"
thisAccount={this.props.collectionsAccount}/>
<hr/>
</div>
<TabView
pages={[
{
header: "Associated Repo Accounts",
element: <AdminAccountLookupReposTab goToAccount={this.goToAccount}
associatedRepoAccounts={this.state.associatedRepoAccounts}/>
},
{
header: "Processing Receipt History",
element: <ProcessingReceiptTable processingReceipts={this.state.processingReceipts}/>,
}]}
/>
</div>
);
}
}
const createPushToRepoButton = (collectionsState: AsyncState, onClickAction: Function, didAddToQueue: boolean | undefined) => {
if (collectionsState === AsyncState.Success) {
return (
<button
data-test="put-to-queue-button-found"
className="btn btn-primary mb-3 ml-auto"
onClick={() => {
onClickAction();
}}
disabled={didAddToQueue}
>
Push To Repo Queue
</button>
);
} else {
return (
<button
data-test="put-to-queue-button-not-found"
className="btn btn-primary mb-3 ml-auto"
disabled={true}
>
Push To Repo Queue
</button>
);
}
};
const createPutToQueueNotifications = (addToQueueState: AsyncState) => {
if (addToQueueState === AsyncState.Loading) {
return (
<div className="mt-2 mb-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Loading}
text={`Adding To Repo Queue`}
data-test="add-to-queue-loading"
/>
</div>
);
}
if (addToQueueState === AsyncState.Success) {
return (
<div className="mt-2 mb-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Info}
text={`Added Account To RepoQueue`}
data-test="add-to-queue-success"
/>
</div>
);
}
if (addToQueueState === AsyncState.Failure) {
return (
<div className="mt-2 mb-1">
<NotificationBar
key="alert-notification"
type={NotificationBarType.Danger}
text={`Failed To Add Account To Repo Queue`}
data-test="add-to-queue-error"
/>
</div>
);
}
};
export const _AdminAccountLookup = AdminAccountLookup;
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminAccountLookup);

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