Adding
This commit is contained in:
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
+45
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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 |
+120
@@ -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);
|
||||
|
||||
});
|
||||
});
|
||||
+78
@@ -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;
|
||||
+9
@@ -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;
|
||||
}
|
||||
+181
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+144
@@ -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;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#auction-house-location-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
#collateral-center-btn {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
+11
@@ -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", () => {});
|
||||
});
|
||||
+244
@@ -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;
|
||||
+14
@@ -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;
|
||||
}
|
||||
+131
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+110
@@ -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;
|
||||
+88
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+96
@@ -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"/>
|
||||
|
||||
<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;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
.is-collateral-primary-check,
|
||||
.is-collateral-primary-check * {
|
||||
cursor: pointer;
|
||||
}
|
||||
+217
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+146
@@ -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;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.collateral-view-row p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
+116
@@ -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('');
|
||||
});
|
||||
|
||||
});
|
||||
+59
@@ -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;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.vin-disp {
|
||||
font-size: smaller;
|
||||
}
|
||||
+31
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
+23
@@ -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);
|
||||
|
||||
+67
@@ -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 © {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"> <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;
|
||||
+8
@@ -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%);
|
||||
}
|
||||
+217
@@ -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);
|
||||
});
|
||||
});
|
||||
+184
@@ -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;
|
||||
+20
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
+96
@@ -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;
|
||||
+140
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
+40
@@ -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;
|
||||
+16
@@ -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;
|
||||
}
|
||||
+59
@@ -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)
|
||||
});
|
||||
});
|
||||
+51
@@ -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>.
|
||||
|
||||
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;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.warning-info-page {
|
||||
padding-top: 15em;
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
p {
|
||||
font-weight: 100;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
+19
@@ -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;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import WarningInfoPage from "../../components/warning-info-page/WarningInfoPage";
|
||||
|
||||
const AccessDeniedPage: React.FC = () => {
|
||||
return (
|
||||
<WarningInfoPage
|
||||
title="Access Denied"
|
||||
body="Please contact IT Operations if this is an error."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessDeniedPage;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.account-activity {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.account-activity-notable-dates {
|
||||
h5 {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import { CustomAsyncNotification } from "lcu-react-lib";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { HistorySource, HistoryType, ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
|
||||
import { ICollateral, IRepoAccount, IRepoStatusChange } from "../../services/service-types/RepoAccountTypes";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {_AccountActivity} from "./AccountActivity";
|
||||
|
||||
describe("AccountActivity component", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const accountHistories: IAsyncStateContainer<ITemenosHistory[]> = {
|
||||
state: AsyncState.Success,
|
||||
data: [{
|
||||
Source: HistorySource.Account,
|
||||
Name: "status",
|
||||
Created: moment("1997-02-05"),
|
||||
Type: HistoryType.Comment,
|
||||
UserId: 0,
|
||||
Message: "Mesage"
|
||||
|
||||
},
|
||||
{
|
||||
Source: HistorySource.Account,
|
||||
Name: "status",
|
||||
Created: moment("1997-02-05"),
|
||||
Type: HistoryType.Comment,
|
||||
UserId: 0,
|
||||
Message: "Mesage"
|
||||
|
||||
},
|
||||
{
|
||||
Source: HistorySource.Account,
|
||||
Name: "status",
|
||||
Created: moment("1997-02-05"),
|
||||
Type: HistoryType.Letter,
|
||||
UserId: 0,
|
||||
Message: "Mesage"
|
||||
}]
|
||||
};
|
||||
const caseHistories: IAsyncStateContainer<ITemenosHistory[]> = {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<_AccountActivity
|
||||
selectedAccount={{
|
||||
accountNo: "1234",
|
||||
collaterals: [] as ICollateral[],
|
||||
repoStatusChanges: [] as IRepoStatusChange[],
|
||||
} as IRepoAccount}
|
||||
alerts={[]}
|
||||
actions={[]}
|
||||
markAlertRead={()=> {}}
|
||||
accountHistories={accountHistories}
|
||||
caseHistories={caseHistories}
|
||||
saveRepoAccount={() => {}}
|
||||
removeAccountFromAlertFilter={()=>{}}
|
||||
externalUserMap={[]}
|
||||
/>);
|
||||
});
|
||||
|
||||
it("Displays all components", () => {
|
||||
expect(wrapper.find(`[data-test='case-notification']`).length).toBe(1);
|
||||
expect(wrapper.find(`[data-test='account-notification']`).length).toBe(1);
|
||||
expect(wrapper.find(`[data-test='repo-letters']`).length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
it("Filters for only letters history for alerts and table", ()=>{
|
||||
expect(wrapper.find(`[data-test='case-notification']`).prop("container").data.length).toBe(0);
|
||||
expect(wrapper.find(`[data-test='account-notification']`).prop("container").data.length).toBe(1);
|
||||
expect(wrapper.find(`[data-test='repo-letters']`).length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
it("Displays Custom Info Notification When Response Array is Emtpy", () => {
|
||||
var customCaseNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='case-notification']`).prop("customNotifications");
|
||||
expect(customCaseNotification).not.toBe(null);
|
||||
expect(customCaseNotification[0].displayWhen(caseHistories.data)).toBe(true);
|
||||
|
||||
var customAccountNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='account-notification']`).prop("customNotifications");
|
||||
expect(customAccountNotification).not.toBe(null);
|
||||
expect(customAccountNotification[0].displayWhen(accountHistories.data)).toBe(false);
|
||||
});
|
||||
|
||||
it("Displays Custom Info Notification When Response Array is Emtpy", () => {
|
||||
//Previous Test With Data Flipped to ensure both have expected behaviour
|
||||
var customCaseNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='case-notification']`).prop("customNotifications");
|
||||
expect(customCaseNotification).not.toBe(null);
|
||||
expect(customCaseNotification[0].displayWhen(accountHistories.data)).toBe(false);
|
||||
|
||||
var customAccountNotification: CustomAsyncNotification[] = wrapper.find(`[data-test='account-notification']`).prop("customNotifications");
|
||||
expect(customAccountNotification).not.toBe(null);
|
||||
expect(customAccountNotification[0].displayWhen(caseHistories.data)).toBe(true);
|
||||
});
|
||||
});
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import {AsyncNotification, NotificationType} from "lcu-react-lib";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { markAlertRead } from "../../actions/AlertActions";
|
||||
import { putRepoAccount, removeRepoAccountFromAlertFilter } from "../../actions/RepoAccountActions";
|
||||
import { IAlert } from "../../reducers/AlertReducer";
|
||||
import { IExternalUser } from "../../reducers/SettingsReducer";
|
||||
import { HistoryType, ITemenosHistory } from "../../services/service-types/AccountHistoryTypes";
|
||||
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import HistoryTable from "../history-table/HistoryTable";
|
||||
import AccountActivityNotifications from "./account-activity-notifications/AccountActivityNotifications";
|
||||
import "./AccountActivity.scss";
|
||||
import NotableDates from "./notable-dates/NotableDates";
|
||||
|
||||
interface IAccountActivityProps {
|
||||
selectedAccount: IRepoAccount;
|
||||
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
|
||||
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
|
||||
externalUserMap: IExternalUser[];
|
||||
}
|
||||
|
||||
interface IPropsFromState {
|
||||
alerts: IAlert[];
|
||||
actions: IAlert[];
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
alerts: state.alertState.alerts,
|
||||
actions: state.alertState.actions,
|
||||
};
|
||||
};
|
||||
|
||||
interface IPropsFromDispatch {
|
||||
markAlertRead: (alert: IAlert) => any;
|
||||
saveRepoAccount: (repo: IRepoAccount) => any;
|
||||
removeAccountFromAlertFilter: (alertMessage: string, repoId: number) => any;
|
||||
}
|
||||
|
||||
const mapDispatchToProps: IPropsFromDispatch = {
|
||||
markAlertRead: markAlertRead,
|
||||
saveRepoAccount: putRepoAccount,
|
||||
removeAccountFromAlertFilter: removeRepoAccountFromAlertFilter
|
||||
};
|
||||
|
||||
type Props = IPropsFromState & IAccountActivityProps & IPropsFromDispatch;
|
||||
|
||||
const AccountActivity: React.FC<Props> = (props) => {
|
||||
const filterOnlyAccountAlerts = (alerts: IAlert[]): IAlert[] => {
|
||||
return alerts.filter((alert) => alert.repoId === props.selectedAccount.repoId);
|
||||
};
|
||||
|
||||
const dismissAlert = (alert: IAlert) => {
|
||||
props.markAlertRead(alert);
|
||||
props.removeAccountFromAlertFilter(alert.message, alert.repoId);
|
||||
};
|
||||
|
||||
const filterOnlyLettersHistories = (histories : IAsyncStateContainer<ITemenosHistory[]>) : IAsyncStateContainer<ITemenosHistory[]> => {
|
||||
if(histories.data){
|
||||
return {
|
||||
state: histories.state,
|
||||
data: histories.data.filter(x => x.Type === HistoryType.Letter)
|
||||
};
|
||||
}
|
||||
|
||||
return histories;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="account-activity container">
|
||||
<AccountActivityNotifications
|
||||
accountAlerts={filterOnlyAccountAlerts(props.alerts)}
|
||||
accountActions={filterOnlyAccountAlerts(props.actions)}
|
||||
onAlertDismiss={dismissAlert}
|
||||
data-test="account-activity-notifications"
|
||||
/>
|
||||
<div className="row">
|
||||
<div className="col-sm-8">
|
||||
<h5>Repo History - Letters</h5>
|
||||
<AsyncNotification
|
||||
container={filterOnlyLettersHistories(props.accountHistories)}
|
||||
subject={"Account Letter Histories"}
|
||||
data-test={"account-notification"}
|
||||
customNotifications={
|
||||
[
|
||||
{
|
||||
displayWhen: (data: ITemenosHistory[]) => {return data.length < 1;},
|
||||
message: `No Account Letters for ${props.selectedAccount.accountNo}`,
|
||||
type: NotificationType.Info
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
<AsyncNotification
|
||||
container={filterOnlyLettersHistories(props.caseHistories)}
|
||||
subject={"Case Letter Histories"}
|
||||
data-test={"case-notification"}
|
||||
customNotifications={
|
||||
[
|
||||
{
|
||||
displayWhen: (data: ITemenosHistory[]) => {return data.length < 1;},
|
||||
message: `No Case Letters for ${props.selectedAccount.accountNo}`,
|
||||
type: NotificationType.Info
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
<HistoryTable
|
||||
accountNo={props.selectedAccount.accountNo}
|
||||
caseHistories={filterOnlyLettersHistories(props.caseHistories)}
|
||||
accountHistories={filterOnlyLettersHistories(props.accountHistories)}
|
||||
externalUserMap={props.externalUserMap}
|
||||
data-test="repo-letters"
|
||||
/>
|
||||
</div>
|
||||
<NotableDates selectedAccount={props.selectedAccount} saveRepoAccount={props.saveRepoAccount} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const _AccountActivity = AccountActivity;
|
||||
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AccountActivity);
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { ShallowWrapper, shallow } from 'enzyme';
|
||||
import AccountActivityNotifications from './AccountActivityNotifications';
|
||||
import { IAlert } from '../../../reducers/AlertReducer';
|
||||
|
||||
describe('AccountActivityNotifications Component', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const dummyAlerts: IAlert[] = [
|
||||
{
|
||||
id: 1,
|
||||
message: "Message",
|
||||
} as IAlert,
|
||||
{
|
||||
id: 1,
|
||||
message: "Message",
|
||||
} as IAlert,
|
||||
];
|
||||
const dummyActions: IAlert[] = [
|
||||
{
|
||||
id: 1,
|
||||
message: "Message",
|
||||
} as IAlert,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<AccountActivityNotifications
|
||||
accountAlerts={dummyAlerts}
|
||||
accountActions={dummyActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it ('Renders Alerts', () => {
|
||||
const alerts = wrapper.find(`[data-test='account-activity-notifications-alert']`);
|
||||
expect(alerts.length).toBe(2);
|
||||
});
|
||||
|
||||
it ('Renders Actions', () => {
|
||||
const actions = wrapper.find(`[data-test='account-activity-notifications-action']`);
|
||||
expect(actions.length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { IAlert } from '../../../reducers/AlertReducer';
|
||||
import NotificationBar, { NotificationBarType } from '../../../components/notification-bar/NotificationBar';
|
||||
|
||||
interface IAcccountActivityNotificationsProps {
|
||||
accountAlerts: IAlert[];
|
||||
accountActions: IAlert[];
|
||||
onAlertDismiss: (alert: IAlert) => any;
|
||||
}
|
||||
|
||||
const AccountActivityNotifications: React.FC<IAcccountActivityNotificationsProps> = (props) => {
|
||||
return (
|
||||
<div className="account-activity-notifications">
|
||||
{
|
||||
props.accountActions.map((action) =>
|
||||
<NotificationBar
|
||||
key={action.id + '-' + action.message}
|
||||
type={NotificationBarType.Info}
|
||||
text={action.message}
|
||||
subText={action.subMessage}
|
||||
fontAwesomeIcon="fa-exclamation"
|
||||
data-test="account-activity-notifications-action"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
props.accountAlerts.map((alert) =>
|
||||
<NotificationBar
|
||||
key={alert.id + '-' + alert.message}
|
||||
type={NotificationBarType.Warning}
|
||||
text={alert.message}
|
||||
subText={alert.subMessage}
|
||||
isButtonPresent={true}
|
||||
onButtonClick={() => props.onAlertDismiss(alert)}
|
||||
fontAwesomeIcon="fa-bell"
|
||||
data-test="account-activity-notifications-alert"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountActivityNotifications;
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.notable-dates {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.notable-dates-edit {
|
||||
margin-left: 2.5em;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
margin-top: 1em;
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import React from "react";
|
||||
import { ShallowWrapper, shallow } from "enzyme";
|
||||
import NotableDates from "./NotableDates";
|
||||
import { IRepoAccount, ICollateral, IRepoStatusChange } from '../../../services/service-types/RepoAccountTypes';
|
||||
import moment from "moment";
|
||||
|
||||
describe('Notable Dates component', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const dummyRepoAccount = {
|
||||
accountNo: '1234',
|
||||
collaterals: [] as ICollateral[],
|
||||
repoStatusChanges: [] as IRepoStatusChange[],
|
||||
} as IRepoAccount
|
||||
|
||||
let dummyUpdatedRepoAccount: IRepoAccount | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<NotableDates
|
||||
selectedAccount={dummyRepoAccount}
|
||||
saveRepoAccount={(x) => dummyUpdatedRepoAccount = x}
|
||||
/>);
|
||||
})
|
||||
|
||||
it ('Renders Existing Dates', () => {
|
||||
wrapper = wrapper.setProps({
|
||||
selectedAccount: {
|
||||
o4RDate: moment('1945-05-07'),
|
||||
redemptionExpiredDate: moment('1945-09-02'),
|
||||
collaterals: [
|
||||
{},
|
||||
{ retitledInLandmarkName: moment('1914-07-28') },
|
||||
],
|
||||
repoStatusChanges: [
|
||||
{ statusTo: 'SOLD', changedDate: moment('1865-04-12') },
|
||||
{ statusTo: 'REPO', changedDate: moment('1918-11-11') }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const o4rDate = wrapper.find(`[data-test='notable-dates-o4r-date']`);
|
||||
expect(o4rDate.length).toBe(1);
|
||||
expect(o4rDate.text()).toContain('05/07/1945');
|
||||
|
||||
const repoDate = wrapper.find(`[data-test='notable-dates-repo-date']`);
|
||||
expect(repoDate.length).toBe(1);
|
||||
expect(repoDate.text()).toContain('11/11/1918');
|
||||
|
||||
const redemDate = wrapper.find(`[data-test='notable-dates-redem-exp-date']`);
|
||||
expect(redemDate.length).toBe(1);
|
||||
expect(redemDate.text()).toContain('09/02/1945');
|
||||
|
||||
const retitledDate = wrapper.find(`[data-test='notable-dates-retitled-date']`);
|
||||
expect(retitledDate.length).toBe(1);
|
||||
expect(retitledDate.text()).toContain('07/28/1914');
|
||||
});
|
||||
|
||||
it ('Renders Non-Existing Dates', () => {
|
||||
const o4rDate = wrapper.find(`[data-test='notable-dates-o4r-date']`);
|
||||
expect(o4rDate.length).toBe(1);
|
||||
expect(o4rDate.text()).toContain('N/A');
|
||||
|
||||
const repoDate = wrapper.find(`[data-test='notable-dates-repo-date']`);
|
||||
expect(repoDate.length).toBe(1);
|
||||
expect(repoDate.text()).toContain('N/A');
|
||||
|
||||
const redemDate = wrapper.find(`[data-test='notable-dates-redem-exp-date']`);
|
||||
expect(redemDate.length).toBe(1);
|
||||
expect(redemDate.text()).toContain('N/A');
|
||||
|
||||
const retitledDate = wrapper.find(`[data-test='notable-dates-retitled-date']`);
|
||||
expect(retitledDate.length).toBe(1);
|
||||
expect(retitledDate.text()).toContain('N/A');
|
||||
});
|
||||
});
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import { ICollateral, IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
|
||||
import RepoAccountService from "../../../services/RepoAccountService";
|
||||
import { Moment } from "moment";
|
||||
import DateFormatting from "../../../shared/formatting/DateFormatting";
|
||||
import DatePicker from 'react-datepicker';
|
||||
import DateConversions from "../../../shared/conversions/DateConversions";
|
||||
import "./NotableDates.css";
|
||||
|
||||
const cloneDeep = require('lodash.clonedeep');
|
||||
|
||||
interface INotableDatesProps {
|
||||
selectedAccount: IRepoAccount;
|
||||
saveRepoAccount: (repo: IRepoAccount) => any;
|
||||
}
|
||||
|
||||
interface INotableDatesState {
|
||||
repoAccount: IRepoAccount;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
type IRepoAccountUpdater<T> = (repoAccount: IRepoAccount, value: T) => void;
|
||||
|
||||
class NotableDates extends React.Component<INotableDatesProps, INotableDatesState> {
|
||||
constructor(props: INotableDatesProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
repoAccount: cloneDeep(props.selectedAccount),
|
||||
isOpen: false
|
||||
}
|
||||
}
|
||||
|
||||
private applyChange<T>(value: T, updater: IRepoAccountUpdater<T>) {
|
||||
const copiedAccount = cloneDeep(this.state.repoAccount);
|
||||
updater(copiedAccount, value);
|
||||
this.setState({
|
||||
repoAccount: copiedAccount,
|
||||
});
|
||||
}
|
||||
|
||||
private handleDateChange(date: Date | null, updater: IRepoAccountUpdater<Moment | undefined>) {
|
||||
const momentDate = DateConversions.dateToMoment(date);
|
||||
this.applyChange(momentDate, updater);
|
||||
}
|
||||
|
||||
toggleOpen = () => this.setState({ isOpen: !this.state.isOpen });
|
||||
|
||||
private formatDate = (date: Moment | undefined = undefined): string => {
|
||||
return date ? DateFormatting.FormatMMDDYYYY(date) : "N/A";
|
||||
};
|
||||
|
||||
private getRetitledDateString = (collaterals: ICollateral[]): string => {
|
||||
for (const collateral of collaterals) {
|
||||
if (collateral.retitledInLandmarkName) {
|
||||
return this.formatDate(collateral.retitledInLandmarkName);
|
||||
}
|
||||
}
|
||||
return this.formatDate();
|
||||
};
|
||||
|
||||
private getRepoDate = (repoAccount: IRepoAccount): string => {
|
||||
return this.formatDate(RepoAccountService.GetLatestStatusChangeDate("REPO", repoAccount));
|
||||
};
|
||||
|
||||
private editButton = () => {
|
||||
return (
|
||||
<button
|
||||
className="notable-dates-edit btn btn-info"
|
||||
onClick={this.toggleOpen}
|
||||
data-test="notable-dates-edit-retitled-date-btn"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="col-sm-4 card notable-dates ml">
|
||||
<h5>Notable Dates:</h5>
|
||||
<hr />
|
||||
<p><b>O4R:</b>
|
||||
<span data-test="notable-dates-o4r-date"> {this.formatDate(this.props.selectedAccount.o4RDate)}</span>
|
||||
</p>
|
||||
<p><b>Repo:</b>
|
||||
<span data-test="notable-dates-repo-date"> {this.getRepoDate(this.props.selectedAccount)}</span>
|
||||
</p>
|
||||
<p><b>Redemption Expired:</b>
|
||||
<span data-test="notable-dates-redem-exp-date"> {this.formatDate(this.props.selectedAccount.redemptionExpiredDate)}
|
||||
</span>
|
||||
{this.state.isOpen ?
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<DatePicker
|
||||
className="form-control date-picker"
|
||||
placeholderText="mm/dd/yyyy"
|
||||
selected={DateConversions.momentToDate(this.state.repoAccount.redemptionExpiredDate)}
|
||||
onChange={(date) => this.handleDateChange(date, (a, v) => a.redemptionExpiredDate = v)}
|
||||
data-test={"notable-dates-redem-exp-date-picker"}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<button
|
||||
className="form-control btn btn-info auction-panel-btn"
|
||||
onClick={() => { this.props.saveRepoAccount(this.state.repoAccount) }}
|
||||
data-test="notable-dates-redem-exp-submit-btn">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</p>
|
||||
<p><b>Retitled:</b>
|
||||
<span data-test="notable-dates-retitled-date"> {this.getRetitledDateString(this.props.selectedAccount.collaterals)}</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NotableDates;
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
import {shallow, ShallowWrapper} from "enzyme";
|
||||
import moment, {Moment} from "moment";
|
||||
import React from "react";
|
||||
import {IRepoAccount} from "../../services/service-types/RepoAccountTypes";
|
||||
import DateCalculations from "../../shared/calculations/DateCalculations";
|
||||
import AccountDetails from "./AccountDetails";
|
||||
|
||||
describe("AccountDetails Container", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const dummyAccount: IRepoAccount = {
|
||||
repoId: 476,
|
||||
accountNo: "string",
|
||||
status: "string",
|
||||
memberNo: 1453,
|
||||
loanNo: 0,
|
||||
accountTypeCode: "acct-type-code",
|
||||
origAccountTypeCode: "",
|
||||
branch: 0,
|
||||
balance: 0,
|
||||
dueToPayDate: moment("1941-12-07"),
|
||||
o4RDate: moment("1945-05-07"),
|
||||
dateFirstDelinquent: moment("1918-11-11"),
|
||||
delinquentAmount: 19.45,
|
||||
daysDelinquent: 101,
|
||||
paidSkipAmount: 0,
|
||||
subStatus: "string",
|
||||
requiredPaymentAmount: 3.14,
|
||||
lstPmtAmt: 1912.00000,
|
||||
origAmount: 42,
|
||||
lastPaymentDate: moment("1865-04-15"),
|
||||
dateFirstIntellect: moment("1963-11-22"),
|
||||
primaryIndividualId: "string",
|
||||
manualRedemptionDate: moment("1812-06-12"),
|
||||
writtenOffDate: moment("1492-01-01"),
|
||||
borrowers: [],
|
||||
collaterals: [],
|
||||
repoStatusChanges: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<AccountDetails
|
||||
selectedAccount={dummyAccount}
|
||||
otherRepos={"0"}
|
||||
branchMapping={[]}
|
||||
refreshCollateral={() => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it("Renders Select Account Fields Correctly", () => {
|
||||
const repoDbDate = wrapper.find(`[data-test='repo-db-date']`);
|
||||
const firstDelinquent = wrapper.find(`[data-test='first-delinquent']`);
|
||||
const otherRepos = wrapper.find(`[data-test='other-repos']`);
|
||||
const daysDelqWhenAdded = wrapper.find(`[data-test='days-delq-when-added']`);
|
||||
const origAmount = wrapper.find(`[data-test='orig-amount']`);
|
||||
const balance = wrapper.find(`[data-test='balance']`);
|
||||
const lastPayment = wrapper.find(`[data-test='last-payment']`);
|
||||
const daysInStatus = wrapper.find(`[data-test='days-in-status']`);
|
||||
const daysCurrentDelq = wrapper.find(`[data-test='days-current-delq']`);
|
||||
const accountCode = wrapper.find(`[data-test='account-code']`);
|
||||
const branch = wrapper.find(`[data-test='branch']`);
|
||||
const currentDelqAmount = wrapper.find(`[data-test='current-delq-amount']`);
|
||||
const lastPmtAmount = wrapper.find(`[data-test='last-pmt-amount']`);
|
||||
const requiredAmount = wrapper.find(`[data-test='required-amount']`);
|
||||
const bankruptcy = wrapper.find(`[data-test='bankruptcy']`);
|
||||
|
||||
expect(repoDbDate.length).toBe(1);
|
||||
expect(repoDbDate.props().value).toEqual("05/07/1945");
|
||||
|
||||
expect(firstDelinquent.length).toBe(1);
|
||||
expect(firstDelinquent.props().value).toEqual("11/11/1918");
|
||||
|
||||
expect(otherRepos.length).toBe(1);
|
||||
expect(otherRepos.props().value).toEqual("0");
|
||||
|
||||
expect(daysDelqWhenAdded.length).toBe(1);
|
||||
expect(daysDelqWhenAdded.props().value).toEqual(101);
|
||||
|
||||
expect(origAmount.length).toBe(1);
|
||||
expect(origAmount.props().value).toEqual("$42.00");
|
||||
|
||||
expect(balance.length).toBe(1);
|
||||
expect(balance.props().value).toEqual("$0.00");
|
||||
|
||||
expect(lastPayment.length).toBe(1);
|
||||
expect(lastPayment.props().value).toEqual("04/15/1865");
|
||||
|
||||
expect(daysInStatus.length).toBe(1);
|
||||
expect(daysInStatus.props().value).toEqual("Missing");
|
||||
|
||||
expect(daysCurrentDelq.length).toBe(1);
|
||||
expect(daysCurrentDelq.props().value).toEqual(
|
||||
DateCalculations.DayDifference(moment("1941-12-07"), moment())
|
||||
);
|
||||
|
||||
expect(accountCode.length).toBe(1);
|
||||
expect(accountCode.props().value).toEqual("(-) / acct-type-code");
|
||||
|
||||
expect(branch.length).toBe(1);
|
||||
expect(branch.props().value).toEqual(0);
|
||||
|
||||
expect(currentDelqAmount.length).toBe(1);
|
||||
expect(currentDelqAmount.props().value).toEqual("$19.45");
|
||||
|
||||
expect(lastPmtAmount.length).toBe(1);
|
||||
expect(lastPmtAmount.props().value).toEqual("$1,912.00");
|
||||
|
||||
expect(requiredAmount.length).toBe(1);
|
||||
expect(requiredAmount.props().value).toEqual("$3.14");
|
||||
|
||||
expect(bankruptcy.length).toBe(1);
|
||||
expect(bankruptcy.props().value).toEqual("No");
|
||||
});
|
||||
|
||||
it("Written Off Date Displays No", () => {
|
||||
|
||||
const dummyAccount: IRepoAccount = {
|
||||
repoId: 476,
|
||||
accountNo: "string",
|
||||
status: "string",
|
||||
memberNo: 1453,
|
||||
loanNo: 0,
|
||||
accountTypeCode: "acct-type-code",
|
||||
origAccountTypeCode: "",
|
||||
branch: 0,
|
||||
balance: 0,
|
||||
dueToPayDate: moment("1941-12-07"),
|
||||
o4RDate: moment("1945-05-07"),
|
||||
dateFirstDelinquent: moment("1918-11-11"),
|
||||
delinquentAmount: 19.45,
|
||||
daysDelinquent: 101,
|
||||
paidSkipAmount: 0,
|
||||
subStatus: "string",
|
||||
requiredPaymentAmount: 3.14,
|
||||
lstPmtAmt: 1912.00000,
|
||||
origAmount: 42,
|
||||
lastPaymentDate: moment("1865-04-15"),
|
||||
dateFirstIntellect: moment("1963-11-22"),
|
||||
primaryIndividualId: "string",
|
||||
manualRedemptionDate: moment("1812-06-12"),
|
||||
borrowers: [],
|
||||
collaterals: [],
|
||||
repoStatusChanges: [],
|
||||
};
|
||||
|
||||
wrapper = shallow(
|
||||
<AccountDetails
|
||||
selectedAccount={dummyAccount}
|
||||
otherRepos={"0"}
|
||||
branchMapping={[]}
|
||||
refreshCollateral={() => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const writtenOffDate = wrapper.find(`[data-test='written-off']`);
|
||||
|
||||
expect(writtenOffDate.length).toBe(1);
|
||||
expect(writtenOffDate.props().value).toBe("No");
|
||||
});
|
||||
|
||||
it("Written Off Date Displays Date", () => {
|
||||
|
||||
wrapper = shallow(
|
||||
<AccountDetails
|
||||
selectedAccount={dummyAccount}
|
||||
otherRepos={"0"}
|
||||
branchMapping={[]}
|
||||
refreshCollateral={() => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const writtenOffDate = wrapper.find(`[data-test='written-off']`);
|
||||
|
||||
expect(writtenOffDate.length).toBe(1);
|
||||
expect(writtenOffDate.props().value).toBe("01/01/1492");
|
||||
});
|
||||
|
||||
it("Renders Account Code with Original Account Code Properly", () => {
|
||||
|
||||
dummyAccount.origAccountTypeCode = "orig-code";
|
||||
const tempwrapper = shallow(
|
||||
<AccountDetails
|
||||
selectedAccount={dummyAccount}
|
||||
otherRepos={"0"}
|
||||
branchMapping={[]}
|
||||
refreshCollateral={() => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const accountCode = tempwrapper.find(`[data-test='account-code']`);
|
||||
expect(accountCode.props().value).toBe("(orig-code) / acct-type-code");
|
||||
});
|
||||
|
||||
it("Renders the Branch Name when a mapping is available", () => {
|
||||
const tempwrapper = shallow(
|
||||
<AccountDetails
|
||||
selectedAccount={dummyAccount}
|
||||
otherRepos={"0"}
|
||||
branchMapping={[{branchId: 0, branchName: "Milwaukee"}]}
|
||||
refreshCollateral={() => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const branch = tempwrapper.find(`[data-test='branch']`);
|
||||
expect(branch.props().value).toBe("Milwaukee");
|
||||
});
|
||||
|
||||
it("Renders the Branch Number when branchMapping is undefined", () => {
|
||||
const tempwrapper = shallow(
|
||||
<AccountDetails
|
||||
selectedAccount={dummyAccount}
|
||||
otherRepos={"0"}
|
||||
branchMapping={[]}
|
||||
refreshCollateral={() => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const branch = tempwrapper.find(`[data-test='branch']`);
|
||||
expect(branch.props().value).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import CollateralDisplay, { ICollateralDisplay } from "../../components/collateral-display/CollateralDisplay";
|
||||
import { IBranchSetEntry } from "../../reducers/SettingsReducer";
|
||||
import RepoAccountService from "../../services/RepoAccountService";
|
||||
import { ICollateral, IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import DateCalculations from "../../shared/calculations/DateCalculations";
|
||||
import DateFormatting from "../../shared/formatting/DateFormatting";
|
||||
import MonetaryFormatting from "../../shared/formatting/MonetaryFormatting";
|
||||
|
||||
interface IAccountDetailsProps {
|
||||
selectedAccount?: IRepoAccount;
|
||||
otherRepos: string;
|
||||
refreshCollateral: (collateral: ICollateral, memberNo: number, loanNo: number) => any;
|
||||
branchMapping: IBranchSetEntry[];
|
||||
}
|
||||
|
||||
const AccountDetails: React.FC<IAccountDetailsProps> = (props) => {
|
||||
|
||||
const getWrittenOffText = () => {
|
||||
const writtenOffDate = getAccountField(acc => acc.writtenOffDate);
|
||||
if (writtenOffDate) {
|
||||
return DateFormatting.FormatMMDDYYYY(writtenOffDate);
|
||||
}
|
||||
return "No";
|
||||
};
|
||||
|
||||
const getAccountField = function<T>(valuePlucker: (repoAccount: IRepoAccount) => T): T | undefined {
|
||||
if (props.selectedAccount) {
|
||||
return valuePlucker(props.selectedAccount);
|
||||
}
|
||||
};
|
||||
|
||||
const getControlledAccountField = function<T>(valuePlucker: (repoAccount: IRepoAccount) => T): T | string {
|
||||
const pluckedVal = getAccountField(valuePlucker);
|
||||
return pluckedVal !== undefined ? pluckedVal : "";
|
||||
};
|
||||
|
||||
const getDaysCurrentDelq = (): string | number => {
|
||||
const dueToPayDate = getAccountField(acc => acc.dueToPayDate);
|
||||
if (dueToPayDate) {
|
||||
return DateCalculations.DayDifference(dueToPayDate, moment());
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const formatAccountTypeCode = (): string => {
|
||||
const accountTypeCode = getControlledAccountField(acc => acc.accountTypeCode);
|
||||
let origAccountTypeCode = getControlledAccountField(acc => acc.origAccountTypeCode);
|
||||
|
||||
origAccountTypeCode = !origAccountTypeCode ? "-" : origAccountTypeCode;
|
||||
|
||||
return "("+origAccountTypeCode+") / " + accountTypeCode;
|
||||
};
|
||||
|
||||
const createCollateralDisplays = (): ICollateralDisplay[] => {
|
||||
if (props.selectedAccount) {
|
||||
return props.selectedAccount.collaterals.map((collateral) => {
|
||||
return {
|
||||
collateral: collateral,
|
||||
refreshCollateral: () => {
|
||||
return props.refreshCollateral(collateral, props.selectedAccount!.memberNo, props.selectedAccount!.loanNo);
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getBranchName = (): string | number =>{
|
||||
const BranchIdFromAccount = getControlledAccountField(acc => acc.branch);
|
||||
|
||||
if(!props.branchMapping){
|
||||
return BranchIdFromAccount;
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const mappedEntry = props.branchMapping.find(branch => branch.branchId == BranchIdFromAccount);
|
||||
|
||||
return (mappedEntry && mappedEntry.branchName) ? mappedEntry.branchName : BranchIdFromAccount;
|
||||
};
|
||||
|
||||
const getSixtyPlusCollector = (): string | undefined =>{
|
||||
const SixtyPlusCollector = getAccountField(acct => acct.sixtyPlusCollector);
|
||||
|
||||
if(SixtyPlusCollector){
|
||||
return SixtyPlusCollector.sixtyPlusCollectorName;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
const isAccountClosed = RepoAccountService.IsAccountClosed(props.selectedAccount);
|
||||
|
||||
return (
|
||||
<div className="account-details-holder container">
|
||||
<CollateralDisplay isAccountClosed={isAccountClosed} collateral={createCollateralDisplays()} />
|
||||
<div className="account-page-input-section row">
|
||||
<div className="col-sm">
|
||||
<p>Repo DB Date</p>
|
||||
<input data-test="repo-db-date" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.o4RDate))} disabled={true} />
|
||||
|
||||
<p>Orig Amount</p>
|
||||
<input data-test="orig-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.origAmount))} disabled={true} />
|
||||
|
||||
<p>Account Code</p>
|
||||
<input data-test="account-code" type="text" className="form-control" value={formatAccountTypeCode()} disabled={true} />
|
||||
|
||||
<p>Bankruptcy</p>
|
||||
<input data-test="bankruptcy" type="text" className="form-control" value={getAccountField(acc => acc.bankruptcy) || "No"} disabled={true} />
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>First Delinquent</p>
|
||||
<input data-test="first-delinquent" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.dateFirstDelinquent))} disabled={true} />
|
||||
|
||||
<p>Balance</p>
|
||||
<input data-test="balance" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.balance))} disabled={true} />
|
||||
|
||||
<p>Branch</p>
|
||||
<input data-test="branch" type="text" className="form-control" value={getBranchName()} disabled={true} />
|
||||
|
||||
<p>Temenos Account</p>
|
||||
<input data-test="reference-id" type="text" className="form-control" value={getAccountField(acc => acc.referenceId)} disabled={true}/>
|
||||
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>Written Off</p>
|
||||
<input data-test="written-off" type="text" className="form-control" value={getWrittenOffText()} disabled={true} />
|
||||
|
||||
<p>Last Payment</p>
|
||||
<input data-test="last-payment" className="form-control" value={DateFormatting.FormatMMDDYYYY(getAccountField(acc => acc.lastPaymentDate))} disabled={true} />
|
||||
|
||||
<p>Current Delq Amount</p>
|
||||
<input data-test="current-delq-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.delinquentAmount))} disabled={true} />
|
||||
|
||||
<p>Temenos Case</p>
|
||||
<input data-test="reference-case-id" type="text" className="form-control" value={getAccountField(acc => acc.referenceCaseId)} disabled={true}/>
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>Other Repos</p>
|
||||
<input data-test="other-repos" type="text" className="form-control" value={props.otherRepos} disabled={true} />
|
||||
|
||||
<p>Days In Status</p>
|
||||
<input data-test="days-in-status" type="text" className="form-control" value={RepoAccountService.GetDaysInStatus(props.selectedAccount)} disabled={true} />
|
||||
|
||||
<p>Last Pmt Amount</p>
|
||||
<input data-test="last-pmt-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.lstPmtAmt))} disabled={true} />
|
||||
|
||||
<p>Account Last Processed</p>
|
||||
<input data-test="account-last-proccessed" type="text" className="form-control" value={DateFormatting.FormatMMDDYYYHHMMA(getAccountField(acc => acc.accountLastProcessed ))} disabled={true} />
|
||||
</div>
|
||||
<div className="col-sm">
|
||||
<p>Days Delq When Added</p>
|
||||
<input data-test="days-delq-when-added" type="text" className="form-control" value={getControlledAccountField(acc => acc.daysDelinquent)} disabled={true} />
|
||||
|
||||
<p>Days Current Delq</p>
|
||||
<input data-test="days-current-delq" type="text" className="form-control" value={getDaysCurrentDelq()} disabled={true} />
|
||||
|
||||
<p>Required Amount</p>
|
||||
<input data-test="required-amount" type="text" className="form-control" value={MonetaryFormatting.FormatUSD(getAccountField(acc => acc.requiredPaymentAmount))} disabled={true} />
|
||||
|
||||
<p>60+ User Assigned</p>
|
||||
<input data-test="sixty-plus-collector" type="text" className="form-control" value={getSixtyPlusCollector()} disabled={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountDetails;
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import {_AccountListView} from "./AccountListView";
|
||||
import AsyncState from '../../shared/enumerations/AsyncState';
|
||||
import { ShallowWrapper, shallow, mount } from 'enzyme';;
|
||||
|
||||
describe('AccountListView Container', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<_AccountListView
|
||||
repoAccounts={{
|
||||
state: AsyncState.Inactive,
|
||||
data: []
|
||||
}}
|
||||
filteredAccountIds={{ state: AsyncState.Inactive }}
|
||||
accountFilters={{}}
|
||||
goToNextRepoAccountPage={() => {}}
|
||||
goToPreviousRepoAccountPage={() => {}}
|
||||
history={{} as any}
|
||||
location={{} as any}
|
||||
match={{} as any}
|
||||
/>
|
||||
);
|
||||
});
|
||||
it('Renders AccountListPagedTable', () => {
|
||||
var tempWrapper = mount(
|
||||
<_AccountListView
|
||||
repoAccounts={{
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}}
|
||||
filteredAccountIds={{ state: AsyncState.Inactive }}
|
||||
accountFilters={{}}
|
||||
goToNextRepoAccountPage={() => {}}
|
||||
goToPreviousRepoAccountPage={() => {}}
|
||||
history={{} as any}
|
||||
location={{} as any}
|
||||
match={{} as any}
|
||||
/>
|
||||
);
|
||||
|
||||
const component = tempWrapper.find('AccountListPagedTable');
|
||||
expect(component.length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
it ('Renders Non-Success Notification Bar', () => {
|
||||
const nonSuccessNotificationBar = wrapper.find(`[data-test='notification-bar-non-success']`);
|
||||
expect(nonSuccessNotificationBar.length).toBe(1);
|
||||
|
||||
});
|
||||
|
||||
it ('Renders "No Accounts Found" Notification', () => {
|
||||
wrapper = wrapper.setProps({
|
||||
repoAccounts: {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
}
|
||||
});
|
||||
const noAccountsNotificationBar = wrapper.find(`[data-test='notification-bar-no-accounts']`);
|
||||
expect(noAccountsNotificationBar.length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router-dom";
|
||||
import { goToNextRepoAccountPage, goToPreviousRepoAccountPage } from "../../actions/RepoAccountActions";
|
||||
import NotificationBar, { NotificationBarType } from "../../components/notification-bar/NotificationBar";
|
||||
import { getCurrentlyFilteredAccounts } from "../../selectors/RepoAccountSelectors";
|
||||
import { IRepoAccount, IRepoAccountFilters } from "../../services/service-types/RepoAccountTypes";
|
||||
import AsyncState, { mapAsyncStateToNotificationType } from "../../shared/enumerations/AsyncState";
|
||||
import Routes from "../../shared/Routes";
|
||||
import IAsyncStateContainer, { IAsyncStateContainerStrict } from "../../shared/types/IAsyncStateContainer";
|
||||
import { IPagedResponse } from "../../shared/types/IPagedResponse";
|
||||
import {IAppState} from "../../store/Store";
|
||||
import AccountListPagedTable from "./account-list-paged-table/AccountListPagedTable";
|
||||
|
||||
interface IPropsFromState {
|
||||
filteredAccountIds: IAsyncStateContainer<IPagedResponse<number>>;
|
||||
repoAccounts: IAsyncStateContainerStrict<IRepoAccount[]>;
|
||||
accountFilters: IRepoAccountFilters;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
repoAccounts: getCurrentlyFilteredAccounts(state),
|
||||
accountFilters: state.repoAccountState.accountFilters,
|
||||
filteredAccountIds: state.repoAccountState.filteredAccounts
|
||||
};
|
||||
};
|
||||
|
||||
interface IDispatchFromState {
|
||||
goToNextRepoAccountPage: () => any;
|
||||
goToPreviousRepoAccountPage: () => any;
|
||||
}
|
||||
|
||||
const mapDispatchToProps: IDispatchFromState = {
|
||||
goToNextRepoAccountPage: goToNextRepoAccountPage,
|
||||
goToPreviousRepoAccountPage: goToPreviousRepoAccountPage
|
||||
};
|
||||
|
||||
type Props = IPropsFromState & RouteComponentProps & IDispatchFromState;
|
||||
|
||||
export const AccountListView: React.FC<Props> = (props: Props) => {
|
||||
|
||||
const goToAccount = (account: IRepoAccount) => {
|
||||
props.history.push(Routes.Account(account.repoId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
props.repoAccounts.state !== AsyncState.Success
|
||||
?
|
||||
<NotificationBar
|
||||
type={mapAsyncStateToNotificationType(props.repoAccounts.state)}
|
||||
text={props.repoAccounts.state}
|
||||
data-test="notification-bar-non-success"
|
||||
/>
|
||||
|
||||
:
|
||||
<AccountListPagedTable
|
||||
repoAccounts={props.repoAccounts.data}
|
||||
pagedResponse={props.filteredAccountIds.data}
|
||||
onRepoAccountClicked={goToAccount}
|
||||
onPrevCallback={props.goToPreviousRepoAccountPage}
|
||||
onNextCallback={props.goToNextRepoAccountPage}
|
||||
/>
|
||||
}
|
||||
{props.repoAccounts.state === AsyncState.Success && props.repoAccounts.data.length === 0 ?
|
||||
<NotificationBar
|
||||
type={NotificationBarType.Warning}
|
||||
text="No Accounts Found"
|
||||
data-test="notification-bar-no-accounts"
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const _AccountListView = AccountListView;
|
||||
|
||||
// This should be ideally done with componse.
|
||||
// However, the type wasn't being properly inferred.
|
||||
// This may be something worth looking into at a later point
|
||||
export default withRouter(connect<IPropsFromState, IDispatchFromState, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AccountListView));
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import TablePagination from "../../../components/table-pagination/TablePagination";
|
||||
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
|
||||
import { IPagedResponse } from "../../../shared/types/IPagedResponse";
|
||||
import AccountListTable from "../account-list-table/AccountListTable";
|
||||
|
||||
interface IAccountListPagedTableProps {
|
||||
repoAccounts: IRepoAccount[];
|
||||
pagedResponse?: IPagedResponse<any>;
|
||||
onRepoAccountClicked: (repoAccount: IRepoAccount) => any;
|
||||
onPrevCallback: () => any;
|
||||
onNextCallback: () => any;
|
||||
}
|
||||
|
||||
const AccountListPagedTable: React.FC<IAccountListPagedTableProps> = (props) => {
|
||||
return (
|
||||
<div className="account-list-paged-table">
|
||||
<div className="account-table-wrapper">
|
||||
<AccountListTable
|
||||
repoAccounts={props.repoAccounts}
|
||||
onRepoAccountClicked={props.onRepoAccountClicked}
|
||||
/>
|
||||
</div>
|
||||
<TablePagination
|
||||
totalItems={props.pagedResponse ? props.pagedResponse.totalItems : 0}
|
||||
itemsPerPage={props.pagedResponse ? props.pagedResponse.items.length : 0}
|
||||
currentPage={props.pagedResponse ? props.pagedResponse.currentPage : 1}
|
||||
totalPages={props.pagedResponse ? props.pagedResponse.totalPages : 1}
|
||||
isPrevDisabled={props.pagedResponse ? !props.pagedResponse.previousPage : false}
|
||||
isNextDisabled={props.pagedResponse ? !props.pagedResponse.nextPage : false}
|
||||
prevPageCallback={props.onPrevCallback}
|
||||
nextPageCallback={props.onNextCallback}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountListPagedTable;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
.account-list-table {
|
||||
.main-panel {
|
||||
padding: 75px 15px;
|
||||
min-height: calc(101vh - 80px);
|
||||
}
|
||||
|
||||
table.lcu {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead.lcu {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
tbody.lcu {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
tr.lcu {
|
||||
width: 100%;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
tr, tr * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.account-no-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import CollateralTextSummary from "../../../components/collateral/text-summary/CollateralTextSummary";
|
||||
import RepoAccountService from "../../../services/RepoAccountService";
|
||||
import { IRepoAccount } from "../../../services/service-types/RepoAccountTypes";
|
||||
import MonetaryFormatting from "../../../shared/formatting/MonetaryFormatting";
|
||||
import StringFormatting from "../../../shared/formatting/StringFormatting";
|
||||
import "./AccountListTable.scss";
|
||||
|
||||
interface IAccountListTableProps {
|
||||
repoAccounts: IRepoAccount[];
|
||||
onRepoAccountClicked: (repoAccount: IRepoAccount) => any;
|
||||
}
|
||||
|
||||
export const AccountListTable: React.FC<IAccountListTableProps> = (props) => {
|
||||
|
||||
return (
|
||||
<table className="account-list-table table table-striped table-hover">
|
||||
<thead className="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">AccountNo</th>
|
||||
<th scope="col">Borrower</th>
|
||||
<th scope="col">Collateral</th>
|
||||
<th scope="col">LoanBalance</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Days In Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.repoAccounts.map((repoAccount, _) => {
|
||||
return (
|
||||
<tr key={repoAccount.repoId} onClick={() => props.onRepoAccountClicked(repoAccount)}>
|
||||
<td>
|
||||
<p className="account-no-label">{repoAccount.accountNo}</p>
|
||||
{RepoAccountService.IsAccountClosed(repoAccount) ?
|
||||
<p className="text-danger"><b>CLOSED</b></p>
|
||||
: null}
|
||||
</td>
|
||||
<td>{
|
||||
repoAccount.borrowers.map((borrower) => {
|
||||
return (
|
||||
<p key={borrower.individualId}>{StringFormatting.ToTitleCase(borrower.individualName)}</p>
|
||||
);
|
||||
})
|
||||
}</td>
|
||||
<td>{
|
||||
repoAccount.collaterals.map((collateral) => {
|
||||
return (
|
||||
<p key={collateral.collateralId}>
|
||||
<CollateralTextSummary collateral={collateral}/>
|
||||
</p>
|
||||
);
|
||||
})
|
||||
}</td>
|
||||
<td><p>{MonetaryFormatting.FormatUSD(repoAccount.balance)}</p></td>
|
||||
<td><p>{repoAccount.status}</p></td>
|
||||
<td><p>{RepoAccountService.GetDaysInStatus(repoAccount)}</p></td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountListTable;
|
||||
@@ -0,0 +1,49 @@
|
||||
.account-details-holder p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.account-details-holder input {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.tab-view-wrapper {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.account-page-input-section p, .account-page-input-section input {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.account-page-input-section input {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.back-navigation-header .fa-chevron-left {
|
||||
cursor: pointer;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.back-navigation-header * {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.back-navigation-header .row .col-sm:first-child * {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.back-navigation-header .row .col-sm:last-child * {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
.back-navigation-header * {
|
||||
text-align: left;
|
||||
}
|
||||
.back-navigation-header .row .col-sm:last-child * {
|
||||
text-align: left;
|
||||
}
|
||||
.account-page-container .col-sm:not(:first-child) {
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import { _AccountPage } from "./AccountPage";
|
||||
|
||||
{/* - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
|
||||
|
||||
describe("AccountPage component", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const dummy = {} as any;
|
||||
|
||||
const matchParams = {
|
||||
params: {
|
||||
accountno: "123456",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const selectedAccount: IRepoAccount = {
|
||||
repoId: 476,
|
||||
accountNo: "123456",
|
||||
status: "string",
|
||||
memberNo: 1453,
|
||||
loanNo: 0,
|
||||
accountTypeCode: "acct-type-code",
|
||||
branch: 0,
|
||||
balance: 0,
|
||||
dueToPayDate: moment("1941-12-07"),
|
||||
o4RDate: moment("1945-05-07"),
|
||||
dateFirstDelinquent: moment("1918-11-11"),
|
||||
delinquentAmount: 0,
|
||||
daysDelinquent: 101,
|
||||
paidSkipAmount: 0,
|
||||
subStatus: "string",
|
||||
requiredPaymentAmount: 3.14,
|
||||
currentDelinquentAmount: 19.45,
|
||||
lstPmtAmt: 1912.00000,
|
||||
origAmount: 42,
|
||||
lastPaymentDate: moment("1865-04-15"),
|
||||
dateFirstIntellect: moment("1963-11-22"),
|
||||
primaryIndividualId: "string",
|
||||
manualRedemptionDate: moment("1812-06-12"),
|
||||
borrowers: [{
|
||||
repoId: 1,
|
||||
individualId: "1",
|
||||
ownershipType: 6,
|
||||
individualName: "John Doe",
|
||||
}],
|
||||
collaterals: [],
|
||||
repoStatusChanges: [],
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<_AccountPage
|
||||
repoAccounts={[selectedAccount]}
|
||||
allowedStatusChanges={[]}
|
||||
match={matchParams}
|
||||
history={dummy}
|
||||
location={dummy}
|
||||
putRepoAccount={dummy}
|
||||
refreshCollateral={() => {}}
|
||||
selectedAccount={{
|
||||
state: AsyncState.Inactive
|
||||
}}
|
||||
filteredAccounts={{
|
||||
state: AsyncState.Inactive
|
||||
}}
|
||||
fetchSelectedRepoAccount={() => {}}
|
||||
searchRepoAccounts={() => {}}
|
||||
getInitialRepoAccounts={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it ("Renders Account Header", () => {
|
||||
const component = wrapper.find(`[data-test='account-page-header']`);
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it ("Renders Account Details", () => {
|
||||
const accountDetails = wrapper.find(`[data-test='account-page-details']`);
|
||||
expect(accountDetails.length).toBe(1);
|
||||
});
|
||||
|
||||
it ("Renders the Tab View", () => {
|
||||
const tabView = wrapper.find(`[data-test='account-page-tab-view']`);
|
||||
expect(tabView.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
*/}
|
||||
@@ -0,0 +1,182 @@
|
||||
import Axios from "axios";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { compose } from "redux";
|
||||
import { fetchSelectedRepoAccount, putRepoAccount, refreshCollateral } from "../../actions/RepoAccountActions";
|
||||
import AccountPageHeader from "../../components/account-page-header/AccountPageHeader";
|
||||
import { IAllowedStatusChange, IBranchSetEntry } from "../../reducers/SettingsReducer";
|
||||
import { getSelectedAccount } from "../../selectors/RepoAccountSelectors";
|
||||
import { getBranchMapping } from "../../selectors/SettingsSelector";
|
||||
import { ICollateral, IRepoAccount, IRepoAccountMap } from "../../services/service-types/RepoAccountTypes";
|
||||
import Configuration from "../../shared/configuration/Configuration";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import AxiosMessageFormatting from "../../shared/formatting/AxiosMessageFormatting";
|
||||
import UrlFormatting from "../../shared/formatting/UrlFormatting";
|
||||
import ToastDefaults from "../../shared/notifications/ToastDefaults";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import { IAppState } from "../../store/Store";
|
||||
import AccountDetails from "../account-details/AccountDetails";
|
||||
import AccountPageTabView from "./account-page-tab-view/AccountPageTabView";
|
||||
import "./AccountPage.css";
|
||||
|
||||
const cloneDeep = require("lodash.clonedeep");
|
||||
|
||||
export interface IAccountPageRouterProps {
|
||||
readonly repoId: string;
|
||||
}
|
||||
|
||||
interface IPropsFromState {
|
||||
readonly repoAccounts: IRepoAccountMap;
|
||||
readonly allowedStatusChanges: IAllowedStatusChange[];
|
||||
readonly selectedAccount: IAsyncStateContainer<IRepoAccount>;
|
||||
readonly branchMap: IBranchSetEntry[];
|
||||
}
|
||||
|
||||
interface IPropsFromDispatch {
|
||||
readonly refreshCollateral: (collateral: ICollateral, memberNo: number, loanNo: number) => any;
|
||||
readonly putRepoAccount: (repoAccount: IRepoAccount) => any;
|
||||
readonly fetchSelectedRepoAccount: (repoId: number) => any;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
repoAccounts: state.repoAccountState.repoAccounts,
|
||||
allowedStatusChanges: state.settingsState.allowedStatusChanges,
|
||||
selectedAccount: getSelectedAccount(state),
|
||||
branchMap: getBranchMapping(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: IPropsFromDispatch = {
|
||||
fetchSelectedRepoAccount: fetchSelectedRepoAccount,
|
||||
refreshCollateral: refreshCollateral,
|
||||
putRepoAccount: putRepoAccount
|
||||
};
|
||||
|
||||
interface IAccountPageState {
|
||||
numberOfOtherRepos: IAsyncStateContainer<number>;
|
||||
}
|
||||
|
||||
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps<IAccountPageRouterProps>;
|
||||
|
||||
class AccountPage extends React.Component<Props, IAccountPageState> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
numberOfOtherRepos: { state: AsyncState.Loading }
|
||||
};
|
||||
|
||||
this.returnToHomePage = this.returnToHomePage.bind(this);
|
||||
this.saveRepoAccountStatus = this.saveRepoAccountStatus.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchSelectedRepoAccount(parseInt(this.props.match.params.repoId));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (
|
||||
prevProps.selectedAccount.state !== this.props.selectedAccount.state &&
|
||||
this.props.selectedAccount.state === AsyncState.Success &&
|
||||
this.props.selectedAccount.data
|
||||
) {
|
||||
this.loadOtherRepos(this.props.selectedAccount.data.memberNo);
|
||||
}
|
||||
}
|
||||
|
||||
async loadOtherRepos(memberNo: number) {
|
||||
try {
|
||||
// This could possibly return false positives. At some point, a new endpoint should be created
|
||||
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
|
||||
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_search),
|
||||
{ key: "searchPhrase", value: memberNo }
|
||||
);
|
||||
const result = await Axios.get(parameterizedUrl);
|
||||
const historyResult = result.data as IRepoAccount[];
|
||||
this.setState({
|
||||
numberOfOtherRepos: {
|
||||
data: historyResult.length - 1,
|
||||
state: AsyncState.Success,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
|
||||
window.console.error(err);
|
||||
this.setState({
|
||||
numberOfOtherRepos: {
|
||||
state: AsyncState.Failure,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
saveRepoAccountStatus(newStatus: string, selectedRepoAccount?: IRepoAccount): void {
|
||||
if (selectedRepoAccount) {
|
||||
const copiedRepoAccount = cloneDeep(selectedRepoAccount) as IRepoAccount;
|
||||
copiedRepoAccount.status = newStatus;
|
||||
this.props.putRepoAccount(copiedRepoAccount);
|
||||
}
|
||||
}
|
||||
|
||||
returnToHomePage() {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
|
||||
getAccountNoHeader(selectedAccount: IAsyncStateContainer<IRepoAccount>): string {
|
||||
if (selectedAccount.state === AsyncState.Success) {
|
||||
return selectedAccount.data ? selectedAccount.data.accountNo : "-";
|
||||
}
|
||||
return selectedAccount.state;
|
||||
}
|
||||
|
||||
getOtherRepos(): string {
|
||||
if (this.state.numberOfOtherRepos.state === AsyncState.Success && this.state.numberOfOtherRepos.data !== undefined) {
|
||||
return (this.state.numberOfOtherRepos.data).toString();
|
||||
} else {
|
||||
return this.state.numberOfOtherRepos.state;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="account-page-container container-fluid main-panel" data-test="mainPanel">
|
||||
<AccountPageHeader
|
||||
accountNoHeader={this.getAccountNoHeader(this.props.selectedAccount)}
|
||||
selectedRepoAccount={this.props.selectedAccount.data}
|
||||
allowedStatusChanges={this.props.allowedStatusChanges}
|
||||
onBackButtonClicked={this.returnToHomePage}
|
||||
onStatusChange={(newStatus) => this.saveRepoAccountStatus(newStatus, this.props.selectedAccount.data)}
|
||||
putRepoAccount={this.props.putRepoAccount}
|
||||
data-test="account-page-header"
|
||||
/>
|
||||
<AccountDetails
|
||||
selectedAccount={this.props.selectedAccount.data}
|
||||
otherRepos={this.getOtherRepos()}
|
||||
refreshCollateral={this.props.refreshCollateral}
|
||||
branchMapping={this.props.branchMap}
|
||||
data-test="account-page-details"
|
||||
/>
|
||||
<div className="tab-view-wrapper">
|
||||
<AccountPageTabView
|
||||
selectedAccount={this.props.selectedAccount.data}
|
||||
data-test="account-page-tab-view"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability
|
||||
// Use a function composition utility: compose(f, g, h) is the same as (...args) => f(g(h(...args)))
|
||||
const enhance = compose(
|
||||
// connect<ReduxInjected, DispatchInjected, GivenByParentWhenUsingThisComponent, ReduxStoreType>
|
||||
connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)
|
||||
);
|
||||
|
||||
const EnhancedComponent = enhance(AccountPage);
|
||||
|
||||
export const _AccountPage = AccountPage; // Export the non-connected version for ease in unit testing
|
||||
export default EnhancedComponent; // connect(mapStateToProps)(AccountListView);
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import { _AccountPageTabView } from "./AccountPageTabView";
|
||||
|
||||
{/* - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
|
||||
|
||||
describe("AccountPageTabView Container", () => {
|
||||
|
||||
it ("Only Renders Partial Tab if SelectedAccount is Undefined", () => {
|
||||
const wrapper = shallow(
|
||||
<_AccountPageTabView
|
||||
repoCompanies={{ state: AsyncState.Inactive}}
|
||||
putCollateral={() => {}}
|
||||
markRetitled={() => {}}
|
||||
externalUserMap={[]}
|
||||
/>
|
||||
);
|
||||
const emptyTabs = wrapper.find(`[data-test='account-page-tab-view-empty-tabs']`);
|
||||
const loadedTabs = wrapper.find(`[data-test='account-page-tab-view-loaded-tabs']`);
|
||||
expect(emptyTabs.length).toBe(1);
|
||||
expect(loadedTabs.length).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
*/}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/* eslint-disable eqeqeq */
|
||||
import Axios from "axios";
|
||||
import moment from "moment/moment";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {toast} from "react-toastify";
|
||||
import TabView from "../../../components/tab-view/TabView";
|
||||
import {IRepoCompany} from "../../../reducers/RepoCompanyReducer";
|
||||
import {IExternalUser} from "../../../reducers/SettingsReducer";
|
||||
import {HistorySource, ITemenosHistory} from "../../../services/service-types/AccountHistoryTypes";
|
||||
import {IRepoAccount} from "../../../services/service-types/RepoAccountTypes";
|
||||
import Configuration from "../../../shared/configuration/Configuration";
|
||||
import DateConversions from "../../../shared/conversions/DateConversions";
|
||||
import AsyncState from "../../../shared/enumerations/AsyncState";
|
||||
import AxiosMessageFormatting from "../../../shared/formatting/AxiosMessageFormatting";
|
||||
import UrlFormatting from "../../../shared/formatting/UrlFormatting";
|
||||
import ToastDefaults from "../../../shared/notifications/ToastDefaults";
|
||||
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
|
||||
import {IAppState} from "../../../store/Store";
|
||||
import AccountActivity from "../../account-activity/AccountActivity";
|
||||
import AuctionTab from "../../auction-tab/AuctionTab";
|
||||
import RefundStatusTab from "../../refund-status-tab/RefundStatusTab";
|
||||
import RepoHistoryTab from "../../repo-history-tab/RepoHistoryTab";
|
||||
import RepoInfoTab from "../../repo-info-tab/RepoInfoTab";
|
||||
import RepoStatusChangeHistoryTable from "../../repo-status-change-history-table/RepoStatusChangeHistoryTable";
|
||||
import TransactionHistoryTable, {ITransactionHistory} from "../../transaction-history-table/TransactionHistoryTable";
|
||||
import {getExternalUserMap, getRepoTypeMap} from "./../../../selectors/SettingsSelector";
|
||||
|
||||
interface IAccountPageTabViewProps {
|
||||
selectedAccount?: IRepoAccount;
|
||||
}
|
||||
|
||||
interface IPropsFromState {
|
||||
readonly repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
|
||||
readonly externalUserMap: IExternalUser[];
|
||||
readonly repoTypeMap: string[];
|
||||
readonly transactionTypeCodes: string[];
|
||||
readonly transactionTypeCodeQueryPrefix: string;
|
||||
readonly transactionHistoryParameterDateFormat: string;
|
||||
readonly transactionHistoryStartDateOffset: number;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
repoCompanies: state.repoCompanyState.repoCompanies,
|
||||
externalUserMap: getExternalUserMap(state),
|
||||
repoTypeMap: getRepoTypeMap(state),
|
||||
transactionTypeCodes: state.settingsState.transactionTypeCodes,
|
||||
transactionTypeCodeQueryPrefix: state.settingsState.transactionTypeCodeQueryPrefix,
|
||||
transactionHistoryParameterDateFormat: state.settingsState.transactionHistoryParameterDateFormat,
|
||||
transactionHistoryStartDateOffset: state.settingsState.transactionHistoryStartDateOffset
|
||||
};
|
||||
};
|
||||
|
||||
interface IAccountPageState {
|
||||
caseHistories: IAsyncStateContainer<ITemenosHistory[]>;
|
||||
accountHistories: IAsyncStateContainer<ITemenosHistory[]>;
|
||||
transactionHistories: IAsyncStateContainer<ITransactionHistory[]>;
|
||||
}
|
||||
|
||||
type Props = IAccountPageTabViewProps & IPropsFromState;
|
||||
|
||||
class AccountPageTabView extends React.Component<Props, IAccountPageState> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
caseHistories: {state: AsyncState.Inactive},
|
||||
accountHistories: {state: AsyncState.Inactive},
|
||||
transactionHistories: {state: AsyncState.Inactive},
|
||||
};
|
||||
|
||||
this.updateCaseHistory = this.updateCaseHistory.bind(this);
|
||||
this.updateAccountHistory = this.updateAccountHistory.bind(this);
|
||||
this.updateTransactionHistory = this.updateTransactionHistory.bind(this);
|
||||
this.updateHistories = this.updateHistories.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// Update letters if the Selected Account has updated
|
||||
if (this.props.selectedAccount !== prevProps.selectedAccount) {
|
||||
this.updateHistories();
|
||||
}
|
||||
}
|
||||
|
||||
async updateCaseHistory() {
|
||||
if (!this.props.selectedAccount) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.state.caseHistories.state === AsyncState.Inactive ||
|
||||
this.state.caseHistories.state === AsyncState.Failure
|
||||
) {
|
||||
this.setState({
|
||||
caseHistories: {
|
||||
state: AsyncState.Loading,
|
||||
},
|
||||
});
|
||||
if (this.props.selectedAccount.referenceCaseId) {
|
||||
const caseHistories = await this.getCaseHistory(this.props.selectedAccount.referenceCaseId);
|
||||
this.setState({
|
||||
caseHistories: caseHistories,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
caseHistories: {
|
||||
state: AsyncState.Failure
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async updateAccountHistory() {
|
||||
if (!this.props.selectedAccount) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.state.accountHistories.state === AsyncState.Inactive ||
|
||||
this.state.accountHistories.state === AsyncState.Failure
|
||||
) {
|
||||
this.setState({
|
||||
accountHistories: {
|
||||
state: AsyncState.Loading,
|
||||
},
|
||||
});
|
||||
if (this.props.selectedAccount.referenceId) {
|
||||
const accountHistories = await this.getAccountHistory(this.props.selectedAccount.referenceId);
|
||||
this.setState({
|
||||
accountHistories: accountHistories,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
accountHistories: {
|
||||
state: AsyncState.Failure
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateTransactionHistory() {
|
||||
if (!this.props.selectedAccount) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this.state.transactionHistories.state === AsyncState.Inactive ||
|
||||
this.state.transactionHistories.state === AsyncState.Failure
|
||||
) {
|
||||
this.setState({
|
||||
transactionHistories: {
|
||||
state: AsyncState.Loading,
|
||||
},
|
||||
});
|
||||
const transactionHistories = await this.getTransactionHistory(this.props.selectedAccount.accountNo);
|
||||
this.setState({
|
||||
transactionHistories: transactionHistories,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getCaseHistory(caseId: string): Promise<IAsyncStateContainer<ITemenosHistory[]>> {
|
||||
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
|
||||
Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.case_history),
|
||||
{key: "caseId", value: caseId},
|
||||
);
|
||||
try {
|
||||
const result = await Axios.get(parameterizedUrl);
|
||||
const historyResult = result.data as ITemenosHistory[];
|
||||
historyResult.map((x) => {
|
||||
x.Created = DateConversions.utcToMoment(x.Created);
|
||||
x.Source = HistorySource.Case;
|
||||
return x;
|
||||
});
|
||||
return {
|
||||
data: historyResult,
|
||||
state: AsyncState.Success,
|
||||
};
|
||||
} catch (err) {
|
||||
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
|
||||
window.console.error(err);
|
||||
return {
|
||||
state: AsyncState.Failure,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getAccountHistory(accountNo: string): Promise<IAsyncStateContainer<ITemenosHistory[]>> {
|
||||
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
|
||||
Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.account_history),
|
||||
{key: "accountNo", value: accountNo},
|
||||
);
|
||||
try {
|
||||
const result = await Axios.get(parameterizedUrl);
|
||||
const historyResult = result.data as ITemenosHistory[];
|
||||
historyResult.map((x) => {
|
||||
x.Created = DateConversions.utcToMoment(x.Created);
|
||||
x.Source = HistorySource.Account;
|
||||
return x;
|
||||
});
|
||||
return {
|
||||
data: historyResult,
|
||||
state: AsyncState.Success,
|
||||
};
|
||||
} catch (err) {
|
||||
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
|
||||
window.console.error(err);
|
||||
return {
|
||||
state: AsyncState.Failure,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getTransactionHistory(accountNo: string): Promise<IAsyncStateContainer<ITransactionHistory[]>> {
|
||||
const parameterizedUrl = this.getTransactionHistoriesURL(accountNo);
|
||||
if (!parameterizedUrl) {
|
||||
return {
|
||||
state: AsyncState.Failure,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await Axios.get(parameterizedUrl);
|
||||
if (result.data.length < 1)
|
||||
{
|
||||
return result.data;
|
||||
}
|
||||
const transactionHistoryResult = result.data as ITransactionHistory[];
|
||||
transactionHistoryResult.map((x) => {
|
||||
x.postingDate = DateConversions.utcToMoment(x.postingDate);
|
||||
x.effectiveDate = DateConversions.utcToMoment(x.effectiveDate);
|
||||
return x;
|
||||
});
|
||||
return {
|
||||
data: transactionHistoryResult,
|
||||
state: AsyncState.Success,
|
||||
};
|
||||
} catch (err) {
|
||||
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
|
||||
window.console.error(err);
|
||||
return {
|
||||
state: AsyncState.Failure,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getTransactionHistoriesURL(accountNo: string): string | undefined {
|
||||
return UrlFormatting.ReplaceURLParams(
|
||||
Configuration.PrefixCoreUrl(Configuration.Get.urls.core.loan_transactions),
|
||||
{key: "loanNo", value: accountNo},
|
||||
{key: "startDate", value: moment().add(this.props.transactionHistoryStartDateOffset, "d").format(this.props.transactionHistoryParameterDateFormat)},
|
||||
{key: "endDate", value: moment().format(this.props.transactionHistoryParameterDateFormat)}
|
||||
) + UrlFormatting.FormatArrayValuesIntoQuery(this.props.transactionTypeCodes, this.props.transactionTypeCodeQueryPrefix);
|
||||
}
|
||||
|
||||
updateHistories() {
|
||||
this.updateCaseHistory();
|
||||
this.updateAccountHistory();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.selectedAccount) {
|
||||
return (
|
||||
<div data-test="account-page-tab-view-empty-tabs">
|
||||
<TabView
|
||||
pages={[
|
||||
{
|
||||
header: "Activity",
|
||||
element: (<div/>),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div data-test="account-page-tab-view-loaded-tabs">
|
||||
<TabView
|
||||
pages={[
|
||||
{
|
||||
header: "Activity",
|
||||
element: <AccountActivity selectedAccount={this.props.selectedAccount}
|
||||
caseHistories={this.state.caseHistories}
|
||||
accountHistories={this.state.accountHistories}
|
||||
externalUserMap={this.props.externalUserMap}/>,
|
||||
tabOpenCallback: this.updateHistories,
|
||||
},
|
||||
{
|
||||
header: "Repo Info",
|
||||
element: <RepoInfoTab selectedAccount={this.props.selectedAccount}
|
||||
repoCompanies={this.props.repoCompanies.data}
|
||||
repoTypeMap={this.props.repoTypeMap}/>,
|
||||
},
|
||||
{
|
||||
header: "Auction Info",
|
||||
element: <AuctionTab selectedAccount={this.props.selectedAccount}
|
||||
caseHistories={this.state.caseHistories}
|
||||
accountHistories={this.state.accountHistories}/>,
|
||||
tabOpenCallback: this.updateHistories,
|
||||
},
|
||||
{
|
||||
header: "Repo History",
|
||||
element: <RepoHistoryTab accountNo={this.props.selectedAccount.accountNo}
|
||||
accountHistories={this.state.accountHistories}
|
||||
caseHistories={this.state.caseHistories}
|
||||
externalUserMap={this.props.externalUserMap}/>,
|
||||
tabOpenCallback: this.updateHistories
|
||||
},
|
||||
{
|
||||
header: "Transaction History",
|
||||
element: <TransactionHistoryTable accountNo={this.props.selectedAccount.accountNo}
|
||||
transactionHistories={this.state.transactionHistories}/>,
|
||||
tabOpenCallback: this.updateTransactionHistory
|
||||
},
|
||||
{
|
||||
header: "Repo Status Changes",
|
||||
element: <RepoStatusChangeHistoryTable accountNo={this.props.selectedAccount.accountNo}
|
||||
repoStatusChanges={this.props.selectedAccount.repoStatusChanges}/>,
|
||||
},
|
||||
{
|
||||
header: "Refund Status",
|
||||
element: <RefundStatusTab selectedAccount={this.props.selectedAccount}/>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const _AccountPageTabView = AccountPageTabView;
|
||||
export default connect<IPropsFromState, null, {}, IAppState>(mapStateToProps, null)(AccountPageTabView);
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import {shallow, ShallowWrapper} from "enzyme";
|
||||
import { createLocation, createMemoryHistory } from "history";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { match } from "react-router";
|
||||
import {ICollectionsAccount } from "../../reducers/CollectionsAccountReducer";
|
||||
import { IRepoAccount } from "../../services/service-types/RepoAccountTypes";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {_AdminAccountLookup} from "./AdminAccountLookup";
|
||||
|
||||
const searchPhrase = "123-4567";
|
||||
|
||||
const singleCollectionsAccount : IAsyncStateContainer<ICollectionsAccount[]> = {
|
||||
data: [{
|
||||
accountId : 555555,
|
||||
accountIdentifier: "5",
|
||||
accountNumber: "123-4567",
|
||||
accountStatus: "status",
|
||||
accountTypeId: 5,
|
||||
activeFlag: true,
|
||||
bankruptcyFlag: true,
|
||||
branch: "branch",
|
||||
dueDate: moment("2020-12-5"),
|
||||
firstPaymentDueDate: moment("2020-11-5"),
|
||||
originalLoanDate: moment("1995-12-3"),
|
||||
dqAmount: 5,
|
||||
cases: [{
|
||||
caseId: 1,
|
||||
caseNumber: "123",
|
||||
caseDescription: "This is a test",
|
||||
caseSTageId: 5,
|
||||
repoStatus: 2,
|
||||
caseStatusId: 3,
|
||||
caseTitle: "Test",
|
||||
caseTypeBasisId: 4,
|
||||
caseTypeId: 6,
|
||||
createDate: moment("2020-10-5"),
|
||||
createdUserId: 12345,
|
||||
}]
|
||||
}],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
|
||||
const associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]> = {
|
||||
data : [ {repoId: 5,
|
||||
accountNo: "4567",
|
||||
status: "04R",
|
||||
memberNo: 123456,
|
||||
loanNo: 164,
|
||||
accountTypeCode: "typecode",
|
||||
branch: 12,
|
||||
balance: 100.00,
|
||||
delinquentAmount: 150,
|
||||
daysDelinquent: 2,
|
||||
requiredPaymentAmount: 105.00,
|
||||
origAmount: 1,
|
||||
primaryIndividualId: "indvid",
|
||||
borrowers: [],
|
||||
collaterals: [],
|
||||
repoStatusChanges: [],
|
||||
origAccountTypeCode: "test"
|
||||
}],
|
||||
state: AsyncState.Success
|
||||
};
|
||||
|
||||
const didAddToQueue: IAsyncStateContainer<boolean> = {
|
||||
data: false,
|
||||
state: AsyncState.Inactive
|
||||
};
|
||||
|
||||
const history = createMemoryHistory();
|
||||
const path = `/route/:id`;
|
||||
|
||||
const routematch: match<{ id: string }> = {
|
||||
isExact: false,
|
||||
path,
|
||||
url: path.replace(":id", "1"),
|
||||
params: { id: "1" }
|
||||
};
|
||||
|
||||
const location = createLocation(routematch.url);
|
||||
|
||||
describe("AdminAccountLookup Container OK", () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
const combinedStates =
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<_AdminAccountLookup
|
||||
didAddToQueue={didAddToQueue}
|
||||
collectionsAccount={singleCollectionsAccount}
|
||||
searchPhrase={searchPhrase}
|
||||
getCollectionsAccount={()=>{}}
|
||||
setSearchPhraseCollectionsAccounts={()=>{}}
|
||||
putAccountOnRepoQueue={()=>{}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={routematch}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it("Renders The Main Container", () => {
|
||||
const component = wrapper.find(`[data-test='account-lookup-main']`);
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Renders AdminAccountLookupDetails", () =>{
|
||||
const component = wrapper.find(`[data-test='collections-details']`);
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Renders Button For Pushing to Repo Account", () => {
|
||||
const component = wrapper.find(`[data-test='put-to-queue-button-found']`);
|
||||
expect(component.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Renders Put Notification Success",() => {
|
||||
const tempDidAddToQueue = didAddToQueue;
|
||||
tempDidAddToQueue.data = true;
|
||||
tempDidAddToQueue.state = AsyncState.Success;
|
||||
const tempWrapper = shallow(<_AdminAccountLookup
|
||||
didAddToQueue={tempDidAddToQueue}
|
||||
collectionsAccount={singleCollectionsAccount}
|
||||
searchPhrase={searchPhrase}
|
||||
getCollectionsAccount={()=>{}}
|
||||
setSearchPhraseCollectionsAccounts={()=>{}}
|
||||
putAccountOnRepoQueue={()=>{}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={routematch}
|
||||
/>
|
||||
);
|
||||
|
||||
const notification = tempWrapper.find(`[data-test='add-to-queue-success']`);
|
||||
expect(notification.length).toBe(1);
|
||||
const button = tempWrapper.find(`[data-test='put-to-queue-button-found']`);
|
||||
expect(button.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Does Not Render Active Put Button When Collections Is Not Found",() => {
|
||||
const noCollectionsAccount = singleCollectionsAccount;
|
||||
noCollectionsAccount.data = [];
|
||||
noCollectionsAccount.state = AsyncState.Failure;
|
||||
const tempWrapper = shallow(<_AdminAccountLookup
|
||||
didAddToQueue={didAddToQueue}
|
||||
collectionsAccount={noCollectionsAccount}
|
||||
searchPhrase={searchPhrase}
|
||||
getCollectionsAccount={()=>{}}
|
||||
setSearchPhraseCollectionsAccounts={()=>{}}
|
||||
putAccountOnRepoQueue={()=>{}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={routematch}
|
||||
/>);
|
||||
const button = tempWrapper.find(`[data-test='put-to-queue-button-not-found']`);
|
||||
expect(button.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Renders Loading Put To Queue", () => {
|
||||
const loadingPut = didAddToQueue;
|
||||
loadingPut.data = true;
|
||||
loadingPut.state = AsyncState.Loading;
|
||||
const tempWrapper = shallow(<_AdminAccountLookup
|
||||
didAddToQueue={loadingPut}
|
||||
collectionsAccount={singleCollectionsAccount}
|
||||
searchPhrase={searchPhrase}
|
||||
getCollectionsAccount={()=>{}}
|
||||
setSearchPhraseCollectionsAccounts={()=>{}}
|
||||
putAccountOnRepoQueue={()=>{}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={routematch}
|
||||
/>);
|
||||
const notification = tempWrapper.find(`[data-test='add-to-queue-loading']`);
|
||||
expect(notification.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Renders Success Put To Queue", () => {
|
||||
const loadingPut = didAddToQueue;
|
||||
loadingPut.data = true;
|
||||
loadingPut.state = AsyncState.Success;
|
||||
const tempWrapper = shallow(<_AdminAccountLookup
|
||||
didAddToQueue={loadingPut}
|
||||
collectionsAccount={singleCollectionsAccount}
|
||||
searchPhrase={searchPhrase}
|
||||
getCollectionsAccount={()=>{}}
|
||||
setSearchPhraseCollectionsAccounts={()=>{}}
|
||||
putAccountOnRepoQueue={()=>{}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={routematch}
|
||||
/>);
|
||||
const notification = tempWrapper.find(`[data-test='add-to-queue-success']`);
|
||||
expect(notification.length).toBe(1);
|
||||
});
|
||||
|
||||
it("Renders Failed Put To Queue", () => {
|
||||
const loadingPut = didAddToQueue;
|
||||
loadingPut.data = true;
|
||||
loadingPut.state = AsyncState.Failure;
|
||||
const tempWrapper = shallow(<_AdminAccountLookup
|
||||
didAddToQueue={loadingPut}
|
||||
collectionsAccount={singleCollectionsAccount}
|
||||
searchPhrase={searchPhrase}
|
||||
getCollectionsAccount={()=>{}}
|
||||
setSearchPhraseCollectionsAccounts={()=>{}}
|
||||
putAccountOnRepoQueue={()=>{}}
|
||||
history={history}
|
||||
location={location}
|
||||
match={routematch}
|
||||
/>);
|
||||
const notification = tempWrapper.find(`[data-test='add-to-queue-error']`);
|
||||
expect(notification.length).toBe(1);
|
||||
});
|
||||
});
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
import {
|
||||
getCollectionsAccount,
|
||||
putAccountOnRepoQueue,
|
||||
setSearchPhraseCollectionsAccounts
|
||||
} from "../../actions/CollectionsAccountActions";
|
||||
import NotificationBar, {NotificationBarType} from "../../components/notification-bar/NotificationBar";
|
||||
|
||||
import Axios from "axios";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {RouteComponentProps} from "react-router";
|
||||
import {toast} from "react-toastify";
|
||||
import TabView from "../../components/tab-view/TabView";
|
||||
import {ICollectionsAccount} from "../../reducers/CollectionsAccountReducer";
|
||||
import {IRepoAccount} from "../../services/service-types/RepoAccountTypes";
|
||||
import Configuration from "../../shared/configuration/Configuration";
|
||||
import AsyncState from "../../shared/enumerations/AsyncState";
|
||||
import StringFormatting from "../../shared/formatting/StringFormatting";
|
||||
import Routes from "../../shared/Routes";
|
||||
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
|
||||
import {IAppState} from "../../store/Store";
|
||||
import ProcessingReceiptService from "./../../services/ProccessingReceiptService";
|
||||
import {IProcessingReceipt} from "./../../services/service-types/ProcessingReceiptTypes";
|
||||
import DateConversions from "./../../shared/conversions/DateConversions";
|
||||
import AxiosMessageFormatting from "./../../shared/formatting/AxiosMessageFormatting";
|
||||
import UrlFormatting from "./../../shared/formatting/UrlFormatting";
|
||||
import ToastDefaults from "./../../shared/notifications/ToastDefaults";
|
||||
import {ProcessingReceiptTable} from "./../processing-receipt-table/processingReceiptTable";
|
||||
import AdminAccountLookupDetails from "./admin-account-lookup-details/AdminAccountLookupDetails";
|
||||
import {AdminAccountLookupReposTab} from "./admin-account-lookup-repos-tab/AdminAccountLookupReposTab";
|
||||
|
||||
interface IPropsFromState {
|
||||
searchPhrase: string;
|
||||
collectionsAccount: IAsyncStateContainer<ICollectionsAccount[]>;
|
||||
didAddToQueue: IAsyncStateContainer<boolean>;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IAppState): IPropsFromState => {
|
||||
return {
|
||||
collectionsAccount: state.collectionsAccountState.collectionsAccount,
|
||||
searchPhrase: state.collectionsAccountState.searchPhrase,
|
||||
didAddToQueue: state.collectionsAccountState.didAddToQueue,
|
||||
};
|
||||
};
|
||||
|
||||
interface IPropsFromDispatch {
|
||||
getCollectionsAccount: (accountNo: string) => any;
|
||||
setSearchPhraseCollectionsAccounts: (searchPhrase: string) => any;
|
||||
putAccountOnRepoQueue: (collectionsAccount: ICollectionsAccount) => any;
|
||||
}
|
||||
|
||||
const mapDispatchToProps: IPropsFromDispatch = {
|
||||
getCollectionsAccount: getCollectionsAccount,
|
||||
setSearchPhraseCollectionsAccounts: setSearchPhraseCollectionsAccounts,
|
||||
putAccountOnRepoQueue: putAccountOnRepoQueue
|
||||
};
|
||||
|
||||
interface IAdminAccountLookupState {
|
||||
processingReceipts: IAsyncStateContainer<IProcessingReceipt[]>;
|
||||
associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]>;
|
||||
}
|
||||
|
||||
type Props = IPropsFromState & IPropsFromDispatch & RouteComponentProps;
|
||||
|
||||
class AdminAccountLookup extends React.Component <Props, IAdminAccountLookupState> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
processingReceipts: {
|
||||
state: AsyncState.Inactive
|
||||
},
|
||||
associatedRepoAccounts: {
|
||||
state: AsyncState.Inactive
|
||||
}
|
||||
};
|
||||
|
||||
this.handleSearchChange = this.handleSearchChange.bind(this);
|
||||
this.searchAccounts = this.searchAccounts.bind(this);
|
||||
this.handleSearchKeyDown = this.handleSearchKeyDown.bind(this);
|
||||
this.goToAccount = this.goToAccount.bind(this);
|
||||
this.pushToQueue = this.pushToQueue.bind(this);
|
||||
this.updateProcessingReceipts = this.updateProcessingReceipts.bind(this);
|
||||
this.getProcessingReceipts = this.getProcessingReceipts.bind(this);
|
||||
this.getAssociatedRepoAccounts = this.getAssociatedRepoAccounts.bind(this);
|
||||
this.updateProcessingReceipts = this.updateProcessingReceipts.bind(this);
|
||||
}
|
||||
|
||||
goToAccount = (account: IRepoAccount) => {
|
||||
this.props.history.push(Routes.Account(account.repoId));
|
||||
};
|
||||
|
||||
pushToQueue = () => {
|
||||
if (this.props.collectionsAccount && this.props.collectionsAccount.data && this.props.collectionsAccount.data.length === 1) {
|
||||
this.props.putAccountOnRepoQueue(this.props.collectionsAccount.data[0]);
|
||||
}
|
||||
};
|
||||
|
||||
async searchAccounts() {
|
||||
if (this.props.searchPhrase.trim() !== "") {
|
||||
await this.props.getCollectionsAccount(this.props.searchPhrase);
|
||||
await this.updateAssociatedRepoAccounts();
|
||||
await this.updateProcessingReceipts();
|
||||
} else {
|
||||
this.props.collectionsAccount.state = AsyncState.Inactive;
|
||||
this.state.associatedRepoAccounts.state = AsyncState.Inactive;
|
||||
}
|
||||
}
|
||||
|
||||
async handleSearchKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||
if (event.keyCode === 13) { // Enter
|
||||
await this.searchAccounts();
|
||||
}
|
||||
}
|
||||
|
||||
async getAssociatedRepoAccounts(accountNo: string): Promise<IAsyncStateContainer<IRepoAccount[]>> {
|
||||
const paramterizedUrl = UrlFormatting.ReplaceURLParams(
|
||||
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single_by_account_no), {
|
||||
key: "accountNo",
|
||||
value: accountNo
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
const results = await Axios.get(paramterizedUrl);
|
||||
const repoAccounts = results.data as IRepoAccount[];
|
||||
return {
|
||||
data: repoAccounts,
|
||||
state: AsyncState.Success
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.response.status === 404) {
|
||||
return {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
|
||||
window.console.error(err);
|
||||
return {
|
||||
state: AsyncState.Failure
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getProcessingReceipts(accountNo: string): Promise<IAsyncStateContainer<IProcessingReceipt[]>> {
|
||||
const paramterizedUrl = UrlFormatting.ReplaceURLParams(
|
||||
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.account_processing_receipts), {
|
||||
key: "accountNo",
|
||||
value: accountNo
|
||||
}
|
||||
);
|
||||
try {
|
||||
const results = await Axios.get(paramterizedUrl);
|
||||
const processingReceiptResults = results.data as IProcessingReceipt[];
|
||||
processingReceiptResults.map(x => {
|
||||
x.processed = DateConversions.utcToMoment(x.processed);
|
||||
return x;
|
||||
});
|
||||
return {
|
||||
data: ProcessingReceiptService.sortByProcessedDate(processingReceiptResults),
|
||||
state: AsyncState.Success
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.response.status === 404) {
|
||||
return {
|
||||
state: AsyncState.Success,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
toast.error(AxiosMessageFormatting.FormatError(err), ToastDefaults.baseParams);
|
||||
window.console.error(err);
|
||||
return {
|
||||
state: AsyncState.Failure
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async updateProcessingReceipts() {
|
||||
this.setState({
|
||||
processingReceipts: {
|
||||
state: AsyncState.Loading
|
||||
}
|
||||
});
|
||||
|
||||
const processingReceipts = await this.getProcessingReceipts(this.props.searchPhrase);
|
||||
this.setState({
|
||||
processingReceipts: processingReceipts
|
||||
});
|
||||
}
|
||||
|
||||
async updateAssociatedRepoAccounts() {
|
||||
this.setState({
|
||||
associatedRepoAccounts: {
|
||||
state: AsyncState.Loading
|
||||
}
|
||||
});
|
||||
|
||||
const associatedRepoAccounts = await this.getAssociatedRepoAccounts(this.props.searchPhrase);
|
||||
this.setState({
|
||||
associatedRepoAccounts: associatedRepoAccounts
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
handleSearchChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
this.props.setSearchPhraseCollectionsAccounts(StringFormatting.TrimLeft(event.target.value));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="container-fluid main-panel" data-test="account-lookup-main">
|
||||
{createPutToQueueNotifications(this.props.didAddToQueue.state)}
|
||||
<div>
|
||||
<h2>Account Lookup</h2>
|
||||
<div className="form-inline d-flex">
|
||||
<input
|
||||
data-test="text-input"
|
||||
className="form-control mr-sm-2 mb-3"
|
||||
type="search"
|
||||
aria-label="Search"
|
||||
placeholder="Account No"
|
||||
value={this.props.searchPhrase}
|
||||
onChange={this.handleSearchChange}
|
||||
onKeyDown={this.handleSearchKeyDown}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-primary mb-3"
|
||||
type="submit"
|
||||
onClick={this.searchAccounts}
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
{createPushToRepoButton(this.props.collectionsAccount.state, this.pushToQueue, this.props.didAddToQueue.data)}
|
||||
</div>
|
||||
<hr/>
|
||||
<AdminAccountLookupDetails data-test="collections-details"
|
||||
thisAccount={this.props.collectionsAccount}/>
|
||||
<hr/>
|
||||
</div>
|
||||
<TabView
|
||||
pages={[
|
||||
{
|
||||
header: "Associated Repo Accounts",
|
||||
element: <AdminAccountLookupReposTab goToAccount={this.goToAccount}
|
||||
associatedRepoAccounts={this.state.associatedRepoAccounts}/>
|
||||
},
|
||||
{
|
||||
header: "Processing Receipt History",
|
||||
element: <ProcessingReceiptTable processingReceipts={this.state.processingReceipts}/>,
|
||||
}]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const createPushToRepoButton = (collectionsState: AsyncState, onClickAction: Function, didAddToQueue: boolean | undefined) => {
|
||||
if (collectionsState === AsyncState.Success) {
|
||||
return (
|
||||
<button
|
||||
data-test="put-to-queue-button-found"
|
||||
className="btn btn-primary mb-3 ml-auto"
|
||||
onClick={() => {
|
||||
onClickAction();
|
||||
}}
|
||||
disabled={didAddToQueue}
|
||||
>
|
||||
Push To Repo Queue
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
data-test="put-to-queue-button-not-found"
|
||||
className="btn btn-primary mb-3 ml-auto"
|
||||
disabled={true}
|
||||
>
|
||||
Push To Repo Queue
|
||||
</button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const createPutToQueueNotifications = (addToQueueState: AsyncState) => {
|
||||
if (addToQueueState === AsyncState.Loading) {
|
||||
return (
|
||||
<div className="mt-2 mb-1">
|
||||
<NotificationBar
|
||||
key="alert-notification"
|
||||
type={NotificationBarType.Loading}
|
||||
text={`Adding To Repo Queue`}
|
||||
data-test="add-to-queue-loading"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (addToQueueState === AsyncState.Success) {
|
||||
return (
|
||||
<div className="mt-2 mb-1">
|
||||
<NotificationBar
|
||||
key="alert-notification"
|
||||
type={NotificationBarType.Info}
|
||||
text={`Added Account To RepoQueue`}
|
||||
data-test="add-to-queue-success"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (addToQueueState === AsyncState.Failure) {
|
||||
return (
|
||||
<div className="mt-2 mb-1">
|
||||
<NotificationBar
|
||||
key="alert-notification"
|
||||
type={NotificationBarType.Danger}
|
||||
text={`Failed To Add Account To Repo Queue`}
|
||||
data-test="add-to-queue-error"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const _AdminAccountLookup = AdminAccountLookup;
|
||||
export default connect<IPropsFromState, IPropsFromDispatch, {}, IAppState>(mapStateToProps, mapDispatchToProps)(AdminAccountLookup);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user