This commit is contained in:
Zeeshaun
2026-03-17 11:09:46 -05:00
parent bd1a9744ad
commit 6e7edcfe1b
332 changed files with 64174 additions and 0 deletions
@@ -0,0 +1,71 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
.idea
.vs
# VSCode environment details
.vscode
# the version file shouldn't be saved
src/environments/version.ts
@@ -0,0 +1,3 @@
registry=https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/npm/registry/
always-auth=true
@@ -0,0 +1,715 @@
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.2.0](https://landmarkcu.visualstudio.com///compare/v1.1.0...v1.2.0) (2021-01-13)
### Features
* **403/404:** Add 403/404 Pages ([2a9e406](https://landmarkcu.visualstudio.com///commit/2a9e4063b9805542b651320999225321e59f1c44))
* **AccountDetails:** Add OriginalAccountTypeCode information to account details screen ([50e9210](https://landmarkcu.visualstudio.com///commit/50e921008c4897c261b8c3fde3f8981a2656b6b9))
* **AccountDetails:** Use BranchSet to get branch name in AccountDetails ([07bb08b](https://landmarkcu.visualstudio.com///commit/07bb08bb9b043ab1858740345723ec5f62349495))
* **AccountHistoryTable:** Add Account History Table that Combines All Other History Tabs Into One ([a819b39](https://landmarkcu.visualstudio.com///commit/a819b39e05f8e5b8c5c54484015047540a01a58b))
* **AccountHistoryTable:** Improve readability, remove unused col ([46fc158](https://landmarkcu.visualstudio.com///commit/46fc1580fb4c55f8406e3eb5852206e452e22eeb))
* **AccountHistoryTable:** Update Account History Table to use new CollectionsAPI case and account ([b30938c](https://landmarkcu.visualstudio.com///commit/b30938c2cbbce63e016a4c36ef0b70aee24f4c46))
* **AccountHistoryTable:** WIP: Add Account History Table ([e70725f](https://landmarkcu.visualstudio.com///commit/e70725ff6926ba4d9c480723d551b15d576aa36a))
* **AccountPage:** Add button for admins to reopen closed accounts ([8e7620f](https://landmarkcu.visualstudio.com///commit/8e7620f46b7b7f75cf15193f9eb2d6dee04e3571))
* **AdminAccountLookup:** add ability to push to repo queue ([d37ef12](https://landmarkcu.visualstudio.com///commit/d37ef122cedecc22bdb2757f56e7509daf6f5c31))
* **AdminAccountLookup:** add admin account lookup page ([6a86bf2](https://landmarkcu.visualstudio.com///commit/6a86bf2d82708dcb80fb5a0fa821db6e0e79f1b1))
* **AdminAccountLookup:** Add data-test clean up logic ([7864078](https://landmarkcu.visualstudio.com///commit/7864078aac33c7532caa319f428f6a8d49aa1ba4))
* **AdminAccountLookup:** Add ProcessingReceipt tab next to associated repos ([a54d769](https://landmarkcu.visualstudio.com///commit/a54d7696269433f8bc9d95e411051e7cac86f84e))
* **AdminAuctionHouse:** Add Notifications To Auction House Form ([48ac9fd](https://landmarkcu.visualstudio.com///commit/48ac9fd5afdb21e57a125450f285e1eb8536bb03))
* **AdminAuctionHouse:** Add UI for Adding New Auction House to Repo Manager ([e7b0689](https://landmarkcu.visualstudio.com///commit/e7b0689dff21dcdd940bf72aa21d4005e39fb265))
* **AdminAuctionHouses:** Add ability to edit existing auction houses and refactor existing UI ([b2fcfdc](https://landmarkcu.visualstudio.com///commit/b2fcfdc8b91c340416a4106c7646525df7287c97))
* **AdminRepoCompanies:** Add option to view, edit, and add RepoCompany referenceID ([99b8bfa](https://landmarkcu.visualstudio.com///commit/99b8bfaa6d21a99413d1caf7dc8f3a1d90b11e21))
* **alertbar:** Add Checkbox to Alert Bar to filter Closed Accounts ([d2b21a3](https://landmarkcu.visualstudio.com///commit/d2b21a31cf965e7e71fb175a094a35c829f546fe))
* **alertbar:** Add Filter Tags for new Checkbox ([05b387b](https://landmarkcu.visualstudio.com///commit/05b387b0a600a35f349dbd46f2f543a46a23ecd2))
* **CollectionsCaseDetails:** Add case details to account lookup. Adjust UI to fit it neatly. ([de1ba0c](https://landmarkcu.visualstudio.com///commit/de1ba0c3a62fb1b5fcca8d2db96e1d0d9c75f231))
* **HistoryTable:** Use ExternalUsers setting to map UserId to a UserName ([0f9a688](https://landmarkcu.visualstudio.com///commit/0f9a688f5e08865510ce416657bbc062cfaf69c4))
* **ReactLib:** Import LUC React Lib ([bdbbd37](https://landmarkcu.visualstudio.com///commit/bdbbd37c17be36909847b087acdfa249d1469126))
* **RepoTypeMap:** Pull RepoType in from the settings so that it is in sync with RepoApi ([ce2c78c](https://landmarkcu.visualstudio.com///commit/ce2c78c0655f78b078115e2a92520d6a29af8a89))
### Bug Fixes
* **AccountHistoryTable:** Make Data Load into Table As Soon As Its Available ([43e4bd5](https://landmarkcu.visualstudio.com///commit/43e4bd523c29f32cd9d9c3bcc91c149b0c5db041))
* **AdminAccountLookup:** Catch 404 error to display notification not error ([afc5116](https://landmarkcu.visualstudio.com///commit/afc51163f62cb6056712848753da2f8318f7fa01))
* **AdminAccountLookup:** Clean Imports ([deeb7ea](https://landmarkcu.visualstudio.com///commit/deeb7ea663d9e08c48f1fd6deff251ea91c043c5))
* **AdminAccountLookup:** Correctly send account identifier ([a6ad1f3](https://landmarkcu.visualstudio.com///commit/a6ad1f3b9f712903f883c1d567c1d84c8622091e))
* **AdminAccountLookup:** fix bug where push to repo button was trying to go to account ([ff44558](https://landmarkcu.visualstudio.com///commit/ff445586f7b7383b719e2447caf0b6e79b268ee1))
* **AdminAccountLookup:** Remove LoanOfficer from CollectionsAccount. It is not useful. ([c4a234b](https://landmarkcu.visualstudio.com///commit/c4a234bba91ff982cf99bf0deff25f89364daeb7))
* **AlertBar:** Alert Bar will update alerts on load instead of on refresh ([4e57a44](https://landmarkcu.visualstudio.com///commit/4e57a445a14f6027a6fd71dc08b9fa6cb08b7c4b))
* **AlertBar:** Update UseEffect dependencies so it only loads Actions/Alerts on Initial Render ([6f8bd7a](https://landmarkcu.visualstudio.com///commit/6f8bd7aa2ddcd71dc5f3c65d3af2cc0b9844d81d))
* **AlertBarDrowndowns:** Remove console log for debugging ([dd1d21a](https://landmarkcu.visualstudio.com///commit/dd1d21ad65f6410c9eb576bea9f37127199ab494))
* **AlertEvent:** Implement UseEffect hook to load Alerts and Notifications when Component Mounts ([c927c83](https://landmarkcu.visualstudio.com///commit/c927c835b65a37f72f39ad860f91d00039670d24))
* **Alerts:** Remove Account from List After Alert Dismiss ([9d7eef4](https://landmarkcu.visualstudio.com///commit/9d7eef46c4b5424efb9d7a21b26452a24bac0395)), closes [#3595](https://landmarkcu.visualstudio.com///issues/3595)
* **AuctionHouseReducer:** Fix AuctionHouseReducer to properly add new auction house to maintained li ([66767c7](https://landmarkcu.visualstudio.com///commit/66767c7c993fe339866ff762ab8e216b765f27fd))
* **build:** Remove Tracking on Build Directory ([622b4d1](https://landmarkcu.visualstudio.com///commit/622b4d1d49ac7c9a5eeaa75576a01436e920d11d))
* **environment:** replace base URLS fro core and repoApi ([30dca3e](https://landmarkcu.visualstudio.com///commit/30dca3e05b9deae31e7f939872f84af07c9407f4))
* **environment:** Update CollectionsAccountNoPrefix ([1009ef6](https://landmarkcu.visualstudio.com///commit/1009ef604718d82297823e845f96f9ec97ff53b7))
* **Environment.Dev:** restore core qa url ([79f531e](https://landmarkcu.visualstudio.com///commit/79f531e6c78bd99da8e70785398a0edeebdf0ad1))
* **environments:** Restore core and repoapi urls for qa ([6ae7970](https://landmarkcu.visualstudio.com///commit/6ae79703410f1c1ec77bd93dcf6e8bb9589e6a25))
* **HistoryTable:** Use Reference ID instead of ID to refer to user ([61956cb](https://landmarkcu.visualstudio.com///commit/61956cb5ab4ecbb6806bf41066369655fd1adc03))
* **refactor:** change repoApi back to QA ([9743e54](https://landmarkcu.visualstudio.com///commit/9743e547b41750a07483d4f314c392e6eeb9a250))
* **refactor:** fix route reference ([09cf259](https://landmarkcu.visualstudio.com///commit/09cf259088b9ee9c49cde5af4928b8617bcb7753))
* **RepoHistory:** Prevent accountHistory from notifying on caseHistory status. ([d2ddbab](https://landmarkcu.visualstudio.com///commit/d2ddbab0b1dd69056dc4dc0e43bc669b04906f86))
* **RepoInfoTab:** Display RepoCompany name instead of ID in Repo Info Tab ([079071e](https://landmarkcu.visualstudio.com///commit/079071e01c33c9dc225c5e19ca8d284a61891fb2))
* **Router:** Fix user cannot route to floor price calculations ([3fb1e38](https://landmarkcu.visualstudio.com///commit/3fb1e3873aed7ba0d4889df1539571e2265c1274))
* **Security:** on 401, acquire new token ([2157dcb](https://landmarkcu.visualstudio.com///commit/2157dcbc1c931cb8d16ac7619282fccae7a108d9))
* **WrittenOff:** Use written off date from repo account ([149c3fe](https://landmarkcu.visualstudio.com///commit/149c3fed36e087f78ada5167802494eec2fd47e9))
## [1.1.0](https://landmarkcu.visualstudio.com///compare/v1.0.0...v1.1.0) (2019-11-13)
### Bug Fixes
* **Auctions:** Auction Hold no longer changes status to REHO ([7d4d423](https://landmarkcu.visualstudio.com///commit/7d4d423))
* **Env:** Remove Localhost Route ([fff9287](https://landmarkcu.visualstudio.com///commit/fff9287))
* **Env:** Use QA Route instead of locahost ([c56f4a3](https://landmarkcu.visualstudio.com///commit/c56f4a3))
* **ExcelExport:** Export Raw Values ([21dbb4f](https://landmarkcu.visualstudio.com///commit/21dbb4f))
* **Reporting:** Fix Report Sorting Method ([461b1c0](https://landmarkcu.visualstudio.com///commit/461b1c0))
### Features
* **AuctionMap:** Create Center Map on Collateral Button ([d63d35f](https://landmarkcu.visualstudio.com///commit/d63d35f)), closes [#3650](https://landmarkcu.visualstudio.com///issues/3650)
* **FormInputs:** Add Required Form Inputs ([d0679e9](https://landmarkcu.visualstudio.com///commit/d0679e9)), closes [#3629](https://landmarkcu.visualstudio.com///issues/3629)
* **RedemptionExpired:** Manually set date ([f510f8d](https://landmarkcu.visualstudio.com///commit/f510f8d))
* **Reporting:** Implement Sorting & Heat Map ([93a78e4](https://landmarkcu.visualstudio.com///commit/93a78e4)), closes [#3540](https://landmarkcu.visualstudio.com///issues/3540)
### Tests
* **Reporting:** Add Report & Data Table Tests ([12a9854](https://landmarkcu.visualstudio.com///commit/12a9854)), closes [#3540](https://landmarkcu.visualstudio.com///issues/3540)
## [1.0.0](https://landmarkcu.visualstudio.com///compare/v0.13.0...v1.0.0) (2019-11-04)
### Bug Fixes
* **Factoids:** Fix Factoid URL ([dd0e0e1](https://landmarkcu.visualstudio.com///commit/dd0e0e1)), closes [#3548](https://landmarkcu.visualstudio.com///issues/3548)
* **FloorCalculations:** Show Original Calculations ([70dac73](https://landmarkcu.visualstudio.com///commit/70dac73)), closes [#3560](https://landmarkcu.visualstudio.com///issues/3560)
* **IE11:** Allow App to Run in IE11 Again ([0cc6e75](https://landmarkcu.visualstudio.com///commit/0cc6e75)), closes [#3559](https://landmarkcu.visualstudio.com///issues/3559)
* **LegalSaleDate:** Use Redm. Exp. From Column ([5fe09d4](https://landmarkcu.visualstudio.com///commit/5fe09d4)), closes [#3541](https://landmarkcu.visualstudio.com///issues/3541)
### [0.13.2](https://landmarkcu.visualstudio.com///compare/v0.13.1...v0.13.2) (2019-11-01)
### Bug Fixes
* **FloorCalculations:** Show Original Calculations ([70dac73](https://landmarkcu.visualstudio.com///commit/70dac73e05a007f7ef17703478ed55fe5a67ca90)), closes [#3560](https://landmarkcu.visualstudio.com///issues/3560)
* **IE11:** Allow App to Run in IE11 Again ([0cc6e75](https://landmarkcu.visualstudio.com///commit/0cc6e75bd0cf0ff55763e16d9b6f15d92122b047)), closes [#3559](https://landmarkcu.visualstudio.com///issues/3559)
### [0.13.1](https://landmarkcu.visualstudio.com///compare/v0.13.0...v0.13.1) (2019-10-31)
### Bug Fixes
* **Factoids:** Fix Factoid URL ([dd0e0e1](https://landmarkcu.visualstudio.com///commit/dd0e0e148ec6a5bb5729c50dfb8d11408fca6fb2)), closes [#3548](https://landmarkcu.visualstudio.com///issues/3548)
* **LegalSaleDate:** Use Redm. Exp. From Column ([5fe09d4](https://landmarkcu.visualstudio.com///commit/5fe09d486cbf0276d068dfaa8300151e11c49769)), closes [#3541](https://landmarkcu.visualstudio.com///issues/3541)
## [0.13.0](https://landmarkcu.visualstudio.com///compare/v0.12.0...v0.13.0) (2019-10-30)
### Bug Fixes
* **AccountActivity:** Show Alerts via RepoId ([268c376](https://landmarkcu.visualstudio.com///commit/268c376)), closes [#3535](https://landmarkcu.visualstudio.com///issues/3535)
* **EventAlert:** Group on RepoId ([c77ee17](https://landmarkcu.visualstudio.com///commit/c77ee17))
* **Spelling:** Fix grammatical error account close screen ([4f161ac](https://landmarkcu.visualstudio.com///commit/4f161ac))
### Features
* **AccountPage:** Allow User to Clear Retitle Date ([8270ce8](https://landmarkcu.visualstudio.com///commit/8270ce8)), closes [#3532](https://landmarkcu.visualstudio.com///issues/3532)
* **AuctionAssignment:** Allow Saved Location Selection & Saving ([1a4e05a](https://landmarkcu.visualstudio.com///commit/1a4e05a)), closes [#3449](https://landmarkcu.visualstudio.com///issues/3449)
* **RepoCompanies:** Create Admin Page For Repo Companies ([ff14efb](https://landmarkcu.visualstudio.com///commit/ff14efb)), closes [#3199](https://landmarkcu.visualstudio.com///issues/3199)
## [0.12.0](https://landmarkcu.visualstudio.com///compare/v0.11.0...v0.12.0) (2019-10-29)
### Features
* **Reporting:** Add closed account report ([0be1c59](https://landmarkcu.visualstudio.com///commit/0be1c59))
* **Reporting:** Export reports to excel ([232b43f](https://landmarkcu.visualstudio.com///commit/232b43f))
## [0.11.0](https://landmarkcu.visualstudio.com///compare/v0.10.0...v0.11.0) (2019-10-28)
### Bug Fixes
* **SearchFilter:** Toggle Search & Filters ([2f503cd](https://landmarkcu.visualstudio.com///commit/2f503cd)), closes [#3490](https://landmarkcu.visualstudio.com///issues/3490)
### Features
* **Reporting:** Add Active Account Report ([7b80b45](https://landmarkcu.visualstudio.com///commit/7b80b45))
## [0.10.0](https://landmarkcu.visualstudio.com///compare/v0.9.0...v0.10.0) (2019-10-25)
### Bug Fixes
* **AccountDetails:** Populate collector and otherRepos ([55b1c17](https://landmarkcu.visualstudio.com///commit/55b1c17))
* **AccountPage:** Clear filtered accounts on back to home ([cb6014b](https://landmarkcu.visualstudio.com///commit/cb6014b))
* **AuctionAssignment:** Don't Overwrite data on Map Change ([a60bb9f](https://landmarkcu.visualstudio.com///commit/a60bb9f)), closes [#3450](https://landmarkcu.visualstudio.com///issues/3450) [#3479](https://landmarkcu.visualstudio.com///issues/3479)
* **AuctionAssignmentActions:** Standardize void server data to assignment conversion ([e64ade7](https://landmarkcu.visualstudio.com///commit/e64ade7))
* **AuctionHoldInfo:** Add remove hold button ([b359ee5](https://landmarkcu.visualstudio.com///commit/b359ee5))
* **AuctionHoldInfo:** Remove log statement ([6a0179c](https://landmarkcu.visualstudio.com///commit/6a0179c))
* **Dates:** Allow Dates to Work in Internet Explorer ([78dca8f](https://landmarkcu.visualstudio.com///commit/78dca8f)), closes [#3100](https://landmarkcu.visualstudio.com///issues/3100)
* **Dates:** Disregard Timezone ([fe38b89](https://landmarkcu.visualstudio.com///commit/fe38b89)), closes [#3385](https://landmarkcu.visualstudio.com///issues/3385)
* **DeliquentAmt:** Fix use of deliquent amount ([0cee322](https://landmarkcu.visualstudio.com///commit/0cee322))
* **RepoCompanySelection:** Display Null Repo Company ([9d365ae](https://landmarkcu.visualstudio.com///commit/9d365ae)), closes [#3477](https://landmarkcu.visualstudio.com///issues/3477)
### Features
* **Adal:** update adal lib to 5.0 ([ba59fbf](https://landmarkcu.visualstudio.com///commit/ba59fbf))
## [0.9.0](https://landmarkcu.visualstudio.com///compare/v0.8.0...v0.9.0) (2019-10-22)
### Bug Fixes
* **FloorCalculation:** Send FloorPriceValuesUsed to API when recalculating floor ([2ee5c1c](https://landmarkcu.visualstudio.com///commit/2ee5c1c))
### Features
* **StartPage:** Allow further filtering on Start Page ([10ed5fb](https://landmarkcu.visualstudio.com///commit/10ed5fb)), closes [#3409](https://landmarkcu.visualstudio.com///issues/3409) [#3407](https://landmarkcu.visualstudio.com///issues/3407)
## [0.8.0](https://landmarkcu.visualstudio.com///compare/v0.7.0...v0.8.0) (2019-10-22)
### Bug Fixes
* **StatusChangeSelector:** Disable when status is REDEEMED ([022ec20](https://landmarkcu.visualstudio.com///commit/022ec20))
* **URLs:** Remove local temp urls ([bb73b3a](https://landmarkcu.visualstudio.com///commit/bb73b3a))
### Features
* **FloorCalc:** Style Floor Calculation Page ([5f72dc7](https://landmarkcu.visualstudio.com///commit/5f72dc7)), closes [#3339](https://landmarkcu.visualstudio.com///issues/3339)
## [0.7.0](https://landmarkcu.visualstudio.com///compare/v0.6.0...v0.7.0) (2019-10-16)
### Bug Fixes
* **AlertFilter:** Only Filter One Alert at a Time ([37fa198](https://landmarkcu.visualstudio.com///commit/37fa198)), closes [#3355](https://landmarkcu.visualstudio.com///issues/3355)
* **AuctionWizard:** Send RepoID When Saving ([16de7ef](https://landmarkcu.visualstudio.com///commit/16de7ef)), closes [#3224](https://landmarkcu.visualstudio.com///issues/3224)
* **RecalculateFloor:** Bold font and restore url variable ([708b719](https://landmarkcu.visualstudio.com///commit/708b719))
* **RecalculateFloor:** Fix putAuctionAssignment url ([0362841](https://landmarkcu.visualstudio.com///commit/0362841))
### Features
* **AuctionAssignment:** Allow Creation / Removal of Auctions ([0aca35d](https://landmarkcu.visualstudio.com///commit/0aca35d)), closes [#3274](https://landmarkcu.visualstudio.com///issues/3274)
* **FloorPricingParams:** Style Admin Page ([c342ce1](https://landmarkcu.visualstudio.com///commit/c342ce1)), closes [#3338](https://landmarkcu.visualstudio.com///issues/3338)
* **RecalculateFloor:** Mark values used in floor calculation ([d7d6207](https://landmarkcu.visualstudio.com///commit/d7d6207))
* **RecalculateFloor:** Store FloorValuesUsed on Accept Floor ([fdbe57a](https://landmarkcu.visualstudio.com///commit/fdbe57a))
## [0.6.0](https://landmarkcu.visualstudio.com///compare/v0.5.0...v0.6.0) (2019-10-11)
### Bug Fixes
* **Configuration:** Allow Different URl Bases per API ([73dd84f](https://landmarkcu.visualstudio.com///commit/73dd84f)), closes [#3317](https://landmarkcu.visualstudio.com///issues/3317)
* **RecalculateFloor:** Fix css and use Configuration.PrefixBaseUrl ([e432f1d](https://landmarkcu.visualstudio.com///commit/e432f1d))
* **RecalculateFloor:** Fix failing tests ([0e1207b](https://landmarkcu.visualstudio.com///commit/0e1207b))
* **RecalculateFloor:** Fix naming, remove empty interface ([fc37142](https://landmarkcu.visualstudio.com///commit/fc37142))
* **RepoAccounts:** Hot-Fix to new DB Updates ([307b4f5](https://landmarkcu.visualstudio.com///commit/307b4f5)), closes [#3289](https://landmarkcu.visualstudio.com///issues/3289) [#3288](https://landmarkcu.visualstudio.com///issues/3288)
### Features
* **Environment:** Choose Environments based on Hostname ([a5759fc](https://landmarkcu.visualstudio.com///commit/a5759fc)), closes [#3308](https://landmarkcu.visualstudio.com///issues/3308)
* **RecalculateFloor:** Display the values involved in calculation ([cb8dd23](https://landmarkcu.visualstudio.com///commit/cb8dd23))
* **RecalculateFloor:** Initial commit ([fcfcf47](https://landmarkcu.visualstudio.com///commit/fcfcf47))
* **RecalculateFloor:** Initial commit ([f7db41d](https://landmarkcu.visualstudio.com///commit/f7db41d))
* **RepoInfo:** Allow Saving / Update in RepoInfo ([1b710b5](https://landmarkcu.visualstudio.com///commit/1b710b5)), closes [#3297](https://landmarkcu.visualstudio.com///issues/3297)
## 0.5.0 (2019-10-03)
### Bug Fixes
* **AccountList:** show vin along with all collateral ([9dab5b5](https://landmarkcu.visualstudio.com///commit/9dab5b5)), closes [#2864](https://landmarkcu.visualstudio.com///issues/2864)
* **AccountPage:** Allow Non-Loaded Accounts to show on AccountPage ([1915dc0](https://landmarkcu.visualstudio.com///commit/1915dc0)), closes [#2669](https://landmarkcu.visualstudio.com///issues/2669)
* **AccountPage:** Fix Erroneous Loading in AccountPage ([5328674](https://landmarkcu.visualstudio.com///commit/5328674)), closes [#3095](https://landmarkcu.visualstudio.com///issues/3095) [#3109](https://landmarkcu.visualstudio.com///issues/3109)
* **AccountPageTabView:** Unnecessary tab open callback ([4cba028](https://landmarkcu.visualstudio.com///commit/4cba028))
* **Accounts:** Display buttons, account name, collateral correctly ([3978b1f](https://landmarkcu.visualstudio.com///commit/3978b1f)), closes [#2841](https://landmarkcu.visualstudio.com///issues/2841) [#2843](https://landmarkcu.visualstudio.com///issues/2843) [#2844](https://landmarkcu.visualstudio.com///issues/2844)
* **AlertBar:** Remove empty alert boxes ([d0cf36d](https://landmarkcu.visualstudio.com///commit/d0cf36d))
* **AlertBox:** Restrict Alert Box to 5 Rows ([f0af991](https://landmarkcu.visualstudio.com///commit/f0af991)), closes [#2862](https://landmarkcu.visualstudio.com///issues/2862)
* **AuctionAssignment:** Allow Submission of "Car or Light Truck" ([a511fa2](https://landmarkcu.visualstudio.com///commit/a511fa2)), closes [#3070](https://landmarkcu.visualstudio.com///issues/3070)
* **AuctionAssignmentReducer:** Fix merge conflict ([af516f6](https://landmarkcu.visualstudio.com///commit/af516f6))
* **AuctionAssignmentReducer:** Remove log statement ([99fda3b](https://landmarkcu.visualstudio.com///commit/99fda3b))
* **AuctionInfo:** Remove delete button ([312b160](https://landmarkcu.visualstudio.com///commit/312b160))
* **AuctionInfoSidebar:** Fix styling ([898f667](https://landmarkcu.visualstudio.com///commit/898f667))
* **AuctionTab:** Only Show AuctionAssignment Wizard on REPO ([37760e2](https://landmarkcu.visualstudio.com///commit/37760e2)), closes [#2839](https://landmarkcu.visualstudio.com///issues/2839)
* **BigMaps:** Only refresh map on valid prop changes ([ce7e5d2](https://landmarkcu.visualstudio.com///commit/ce7e5d2)), closes [#2838](https://landmarkcu.visualstudio.com///issues/2838)
* **Collateral:** Correct URL for PUT request ([7e7963a](https://landmarkcu.visualstudio.com///commit/7e7963a))
* **CollateralDetails:** Move Year first ([d351cf4](https://landmarkcu.visualstudio.com///commit/d351cf4))
* **Endpoints:** Change endpoint to QA instead of Local ([592f802](https://landmarkcu.visualstudio.com///commit/592f802))
* **FloorPricingParameters:** Add web.config ([bffbf09](https://landmarkcu.visualstudio.com///commit/bffbf09))
* **FloorPricingParams:** Add iisConfig copying in postbuild ([bc516ed](https://landmarkcu.visualstudio.com///commit/bc516ed))
* **FloorPricingParams:** Replace web config with hash routing ([a1ec05c](https://landmarkcu.visualstudio.com///commit/a1ec05c))
* **FloorPricingParams:** Undo index.tsx import ([71d2a81](https://landmarkcu.visualstudio.com///commit/71d2a81))
* **IE11:** Polyfill to allow app to show in IE11 ([1fc66f7](https://landmarkcu.visualstudio.com///commit/1fc66f7))
* **Mapping:** Change bing to use http or https depending on root url ([0458c67](https://landmarkcu.visualstudio.com///commit/0458c67))
* **NavBar:** Hamburger correctly show menus, adjust margins. ([a2590f5](https://landmarkcu.visualstudio.com///commit/a2590f5)), closes [#2845](https://landmarkcu.visualstudio.com///issues/2845)
* **Redeemed:** Disable redeem button when redeemed ([45415b3](https://landmarkcu.visualstudio.com///commit/45415b3))
* **RepoAccount:** Add AlertTypes ([8c3ebc5](https://landmarkcu.visualstudio.com///commit/8c3ebc5))
* **RepoAccount:** Break up large fetch requests ([93c47a8](https://landmarkcu.visualstudio.com///commit/93c47a8))
* **RepoAccountActions:** Update to handle paged response from RepoAccount API ([d6628b2](https://landmarkcu.visualstudio.com///commit/d6628b2))
* **RepoAccountSelector:** *.tes.ts -> *.test.ts ([0d93db5](https://landmarkcu.visualstudio.com///commit/0d93db5))
* **RepoInfoTab:** Cleanup ([ad0198c](https://landmarkcu.visualstudio.com///commit/ad0198c))
* **RepoInfoTab:** Cleanup ([c1d5d0a](https://landmarkcu.visualstudio.com///commit/c1d5d0a))
* **RepoInfoTab:** Correctly populate null repo type ([c8142f5](https://landmarkcu.visualstudio.com///commit/c8142f5))
* **RepoInfoTab:** Remove unused UI elements ([c4676bc](https://landmarkcu.visualstudio.com///commit/c4676bc))
* **RepoInfoTab:** Update dropdown correctly ([b628af2](https://landmarkcu.visualstudio.com///commit/b628af2))
* **SelectedAccount:** Revert SelectedAccount back to SelectedAccountNo to fix dual state issue ([087d903](https://landmarkcu.visualstudio.com///commit/087d903))
* **VehicleFloorPricingParameters:** Update UI ([8f71291](https://landmarkcu.visualstudio.com///commit/8f71291))
### Features
* **AccountActivity:** Add dummy AccountActivity via TabView to AccountPage ([a66d0d8](https://landmarkcu.visualstudio.com///commit/a66d0d8)), closes [#2358](https://landmarkcu.visualstudio.com///issues/2358)
* **AccountActivity:** Differentiate Actions & Alerts ([8c057bd](https://landmarkcu.visualstudio.com///commit/8c057bd)), closes [#3052](https://landmarkcu.visualstudio.com///issues/3052)
* **AccountList:** Show basic list of accounts on main screen ([c490ff9](https://landmarkcu.visualstudio.com///commit/c490ff9)), closes [#2162](https://landmarkcu.visualstudio.com///issues/2162)
* **AccountNotifications:** Add Alerts/Actions to Activity Tab ([7124f08](https://landmarkcu.visualstudio.com///commit/7124f08)), closes [#2885](https://landmarkcu.visualstudio.com///issues/2885)
* **AccountNotifications:** Allow Alerts to be Dismissed ([a6f582c](https://landmarkcu.visualstudio.com///commit/a6f582c)), closes [#2886](https://landmarkcu.visualstudio.com///issues/2886)
* **AccountPage:** Add basic details to AccountPage ([65a08c0](https://landmarkcu.visualstudio.com///commit/65a08c0)), closes [#2330](https://landmarkcu.visualstudio.com///issues/2330)
* **AccountPage:** Add Repo Status Changes tab ([1930fd9](https://landmarkcu.visualstudio.com///commit/1930fd9))
* **AccountPage:** Move information into the header of the AccountPage ([1effb9a](https://landmarkcu.visualstudio.com///commit/1effb9a)), closes [#2400](https://landmarkcu.visualstudio.com///issues/2400)
* **AccountPage:** Show viable status changes for an account using settings ([b1fec9a](https://landmarkcu.visualstudio.com///commit/b1fec9a)), closes [#2548](https://landmarkcu.visualstudio.com///issues/2548)
* **Admin:** Refactor admin to be more than one page ([d74f398](https://landmarkcu.visualstudio.com///commit/d74f398)), closes [#2741](https://landmarkcu.visualstudio.com///issues/2741)
* **AlertBar:** Add alert groups ([d56843a](https://landmarkcu.visualstudio.com///commit/d56843a))
* **AlertBox:** Display alert count ([84f5914](https://landmarkcu.visualstudio.com///commit/84f5914))
* **AlertBox:** Use new IAlert model ([99ce0db](https://landmarkcu.visualstudio.com///commit/99ce0db))
* **AlertRow:** Show related accounts on click ([8fae0b6](https://landmarkcu.visualstudio.com///commit/8fae0b6))
* **AlertRow:** Show related accounts on click ([168fb0b](https://landmarkcu.visualstudio.com///commit/168fb0b))
* **Alerts:** Add basic alerts along with Redux ([a63f11a](https://landmarkcu.visualstudio.com///commit/a63f11a))
* **Alerts:** Add basic alerts along with Redux ([540ac8c](https://landmarkcu.visualstudio.com///commit/540ac8c))
* **Alerts:** Add Sub-Message to Alerts & Notifications ([03b04c3](https://landmarkcu.visualstudio.com///commit/03b04c3)), closes [#3117](https://landmarkcu.visualstudio.com///issues/3117)
* **AppConfig:** Implement AppConfig ([eb1e1c5](https://landmarkcu.visualstudio.com///commit/eb1e1c5))
* **AppSkeleton:** Build out app skeleton, providing an Agile and Architectural starting point ([310f42c](https://landmarkcu.visualstudio.com///commit/310f42c)), closes [#1487](https://landmarkcu.visualstudio.com///issues/1487)
* **AuctionAssignment:** Add Bing Maps to Auction Assignment ([02d93f8](https://landmarkcu.visualstudio.com///commit/02d93f8)), closes [#2822](https://landmarkcu.visualstudio.com///issues/2822)
* **AuctionAssignment:** Add Default Legal Sale Date ([fb927bc](https://landmarkcu.visualstudio.com///commit/fb927bc)), closes [#2992](https://landmarkcu.visualstudio.com///issues/2992)
* **AuctionAssignment:** Allow Creation of new Auction Assignments ([6c27227](https://landmarkcu.visualstudio.com///commit/6c27227)), closes [#2797](https://landmarkcu.visualstudio.com///issues/2797)
* **AuctionAssignment:** Create Auction Assignment UI ([1d1fb70](https://landmarkcu.visualstudio.com///commit/1d1fb70)), closes [#2742](https://landmarkcu.visualstudio.com///issues/2742)
* **AuctionAssignment:** Restrict Pickup Date to Future ([8aad1b8](https://landmarkcu.visualstudio.com///commit/8aad1b8)), closes [#2870](https://landmarkcu.visualstudio.com///issues/2870)
* **AuctionAssignmentWizard:** Show CollectionsHistory on new AuctionAssignmentWizard ([bb29bce](https://landmarkcu.visualstudio.com///commit/bb29bce)), closes [#2728](https://landmarkcu.visualstudio.com///issues/2728)
* **AuctionCollateralLocation:** Display Auction Collateral Location in AuctionAssignmentWizard ([b24076c](https://landmarkcu.visualstudio.com///commit/b24076c)), closes [#2735](https://landmarkcu.visualstudio.com///issues/2735)
* **AuctionCollateralSelection:** Allow User to Specify Collateral ([5ab0661](https://landmarkcu.visualstudio.com///commit/5ab0661)), closes [#2762](https://landmarkcu.visualstudio.com///issues/2762)
* **AuctionHouses:** Create Admin Auction House Page ([ffc0c72](https://landmarkcu.visualstudio.com///commit/ffc0c72)), closes [#2934](https://landmarkcu.visualstudio.com///issues/2934)
* **AuctionInfo:** Create AuctionInfo screen ([05f11c5](https://landmarkcu.visualstudio.com///commit/05f11c5)), closes [#2512](https://landmarkcu.visualstudio.com///issues/2512)
* **AuctionInfo:** Create AuctionInfo screen ([c356ff0](https://landmarkcu.visualstudio.com///commit/c356ff0)), closes [#2512](https://landmarkcu.visualstudio.com///issues/2512)
* **AuctionInfo:** Notify AutoIMS on redeem ([37aae76](https://landmarkcu.visualstudio.com///commit/37aae76))
* **AuctionInfo:** Redeem button changes repo status ([967599e](https://landmarkcu.visualstudio.com///commit/967599e))
* **AuctionInfo:** Refactor redeem button and add test ([93bbbcd](https://landmarkcu.visualstudio.com///commit/93bbbcd))
* **Authorization:** Always pass JWT/Bearer to api ([3b54593](https://landmarkcu.visualstudio.com///commit/3b54593))
* **Collateral:** Mark as retitled ([f3d900b](https://landmarkcu.visualstudio.com///commit/f3d900b))
* **Collateral:** Refresh multiple collateral items ([16745cc](https://landmarkcu.visualstudio.com///commit/16745cc))
* **Collateral:** Refresh single collateral only ([b296ebe](https://landmarkcu.visualstudio.com///commit/b296ebe))
* **CollateralDisplay:** Create CollateralDisplay component. ([4ca0633](https://landmarkcu.visualstudio.com///commit/4ca0633)), closes [#2326](https://landmarkcu.visualstudio.com///issues/2326)
* **CollectionsHistoryTab:** Create CollectionsHistoryTab ([3600e7d](https://landmarkcu.visualstudio.com///commit/3600e7d)), closes [#2674](https://landmarkcu.visualstudio.com///issues/2674)
* **EventAlert:** Replace AlertType with Message string ([58541e3](https://landmarkcu.visualstudio.com///commit/58541e3))
* **EventAlerts:** Show action needed items and basic account list ([0fedaf4](https://landmarkcu.visualstudio.com///commit/0fedaf4)), closes [#1659](https://landmarkcu.visualstudio.com///issues/1659)
* **FloorPricing:** Edit From & To Conditions & Percentage Of ([e930c34](https://landmarkcu.visualstudio.com///commit/e930c34))
* **FloorPricingParameters:** Disallow gaps between from and to conditions ([39c6fdc](https://landmarkcu.visualstudio.com///commit/39c6fdc))
* **Logging:** Add Server Logging from Client ([6e431a8](https://landmarkcu.visualstudio.com///commit/6e431a8)), closes [#2794](https://landmarkcu.visualstudio.com///issues/2794)
* **PostChargedOffHistory:** Create Charged PostChargedOff History Tab ([1822477](https://landmarkcu.visualstudio.com///commit/1822477)), closes [#2650](https://landmarkcu.visualstudio.com///issues/2650)
* **ReactRouter:** Implement ReactRouter into application ([09f74e0](https://landmarkcu.visualstudio.com///commit/09f74e0))
* **RepoAccount:** Shorten account list URL ([c559d50](https://landmarkcu.visualstudio.com///commit/c559d50))
* **RepoAccount:** Update Reducer to Key:Value ([9d302f6](https://landmarkcu.visualstudio.com///commit/9d302f6)), closes [#3044](https://landmarkcu.visualstudio.com///issues/3044)
* **RepoAccountReducer:** Test state updates with retitled collateral ([25405ab](https://landmarkcu.visualstudio.com///commit/25405ab))
* **RepoAccounts:** Add dummy Borrowers & Collateral to RepoAccounts. ([fcdf460](https://landmarkcu.visualstudio.com///commit/fcdf460)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **RepoInfo:** Disabled Disabled Repo Companies ([4df33d4](https://landmarkcu.visualstudio.com///commit/4df33d4)), closes [#2837](https://landmarkcu.visualstudio.com///issues/2837)
* **RepoInfoTab:** Add RepoType selection ([6a45473](https://landmarkcu.visualstudio.com///commit/6a45473))
* **RepoInfoTab:** Add voluntary status to Repo Type ([6ba38ff](https://landmarkcu.visualstudio.com///commit/6ba38ff))
* **RepoInfoTab:** Change Save Icon to Update Button ([003e951](https://landmarkcu.visualstudio.com///commit/003e951)), closes [#2824](https://landmarkcu.visualstudio.com///issues/2824)
* **RepoInfoTab:** Change Save Icon to Update Button ([114d1f9](https://landmarkcu.visualstudio.com///commit/114d1f9)), closes [#2824](https://landmarkcu.visualstudio.com///issues/2824)
* **RepoInfoTab:** Display and allow user to select Repo Company ([7882852](https://landmarkcu.visualstudio.com///commit/7882852)), closes [#2470](https://landmarkcu.visualstudio.com///issues/2470)
* **RepoInfoTab:** Notify title sent ([d8d0eb9](https://landmarkcu.visualstudio.com///commit/d8d0eb9))
* **RepoLettersTable:** Initial commit ([ef298e7](https://landmarkcu.visualstudio.com///commit/ef298e7))
* **RepoLettersTable:** Update to QA API URL ([81ff7b3](https://landmarkcu.visualstudio.com///commit/81ff7b3))
* **Search:** Implement Search ([56f5bf8](https://landmarkcu.visualstudio.com///commit/56f5bf8)), closes [#3087](https://landmarkcu.visualstudio.com///issues/3087)
* **Security:** Require users to member of AD to see app. ([cf1d5ab](https://landmarkcu.visualstudio.com///commit/cf1d5ab)), closes [#1723](https://landmarkcu.visualstudio.com///issues/1723) [#1724](https://landmarkcu.visualstudio.com///issues/1724)
* **Security:** Restrict user for accessing app if they don't have role(s) ([25f6cca](https://landmarkcu.visualstudio.com///commit/25f6cca)), closes [#1723](https://landmarkcu.visualstudio.com///issues/1723) [#1724](https://landmarkcu.visualstudio.com///issues/1724)
* **Setup:** Initial React App package.json setup ([926c7b7](https://landmarkcu.visualstudio.com///commit/926c7b7))
* **SoldBanner:** Add Sold Banner to Auction Assignments ([ecb6ca7](https://landmarkcu.visualstudio.com///commit/ecb6ca7)), closes [#2942](https://landmarkcu.visualstudio.com///issues/2942)
* **StatusChange:** Status Change Now Posts Update ([93dacbd](https://landmarkcu.visualstudio.com///commit/93dacbd)), closes [#2842](https://landmarkcu.visualstudio.com///issues/2842)
* **StatusChange:** Status Change Now Posts Update ([ac0d7c5](https://landmarkcu.visualstudio.com///commit/ac0d7c5)), closes [#2842](https://landmarkcu.visualstudio.com///issues/2842)
* **TabView:** Create TabView Component ([7283796](https://landmarkcu.visualstudio.com///commit/7283796)), closes [#2354](https://landmarkcu.visualstudio.com///issues/2354)
* **Toasts:** Allow actions to toast the user with information. ([bfd9ddd](https://landmarkcu.visualstudio.com///commit/bfd9ddd)), closes [#2715](https://landmarkcu.visualstudio.com///issues/2715)
* **Toasts:** Implement HTTP Failure Toasts ([be6da99](https://landmarkcu.visualstudio.com///commit/be6da99)), closes [#2793](https://landmarkcu.visualstudio.com///issues/2793)
* **TransactionHistoryTab:** Implement Transaction History Tab ([04601e6](https://landmarkcu.visualstudio.com///commit/04601e6)), closes [#2684](https://landmarkcu.visualstudio.com///issues/2684)
* **VehicleFloorPricingParams:** Add component ([997aa77](https://landmarkcu.visualstudio.com///commit/997aa77))
* **Versioning:** Version app, fix up footer, header, and configuration. ([6f7a0bb](https://landmarkcu.visualstudio.com///commit/6f7a0bb)), closes [#2716](https://landmarkcu.visualstudio.com///issues/2716)
* **WrittenOffField:** Add Written Off Field to AccountDetails ([cc9f514](https://landmarkcu.visualstudio.com///commit/cc9f514)), closes [#2698](https://landmarkcu.visualstudio.com///issues/2698)
* **WrittenOffField:** Add Written Off Field to AccountDetails ([2400ed0](https://landmarkcu.visualstudio.com///commit/2400ed0)), closes [#2698](https://landmarkcu.visualstudio.com///issues/2698)
* Implement view transitions between StartPage and AccountPage ([54e5f85](https://landmarkcu.visualstudio.com///commit/54e5f85)), closes [#2321](https://landmarkcu.visualstudio.com///issues/2321)
### Tests
* **AccountDetails:** Add tests to AccountDetails ([44f2466](https://landmarkcu.visualstudio.com///commit/44f2466))
* **AccountDetails:** Add tests to AccountDetails ([101f32f](https://landmarkcu.visualstudio.com///commit/101f32f))
* **AccountPage:** Add tests for getViableStatuses ([02c82cc](https://landmarkcu.visualstudio.com///commit/02c82cc))
* **AlertBox:** Add rendering tests ([29c8a96](https://landmarkcu.visualstudio.com///commit/29c8a96))
* **AlertReducer:** Add unit test ([805d3a6](https://landmarkcu.visualstudio.com///commit/805d3a6))
* **Client:** Add Tests Across Various Components ([41f7e7b](https://landmarkcu.visualstudio.com///commit/41f7e7b))
* **Client:** Add Tests Across Various Components ([102722b](https://landmarkcu.visualstudio.com///commit/102722b))
* **CollateralDisplay:** Fix broken test cases for CollateralDisplay ([28ec130](https://landmarkcu.visualstudio.com///commit/28ec130)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **EventAlert:** EventAlert integration use mock store ([8c9a284](https://landmarkcu.visualstudio.com///commit/8c9a284))
* **EventAlert:** Update event alert tests with real store ([94640b0](https://landmarkcu.visualstudio.com///commit/94640b0))
* **RepoInfoTab:** Implement tests for RepoInfoTab ([c9d074d](https://landmarkcu.visualstudio.com///commit/c9d074d)), closes [#2451](https://landmarkcu.visualstudio.com///issues/2451)
* **SoldBanner:** Add Tests to Sold Banner ([6c38b0c](https://landmarkcu.visualstudio.com///commit/6c38b0c)), closes [#2942](https://landmarkcu.visualstudio.com///issues/2942)
## 0.4.0 (2019-10-03)
### Bug Fixes
* **AccountList:** show vin along with all collateral ([9dab5b5](https://landmarkcu.visualstudio.com///commit/9dab5b5)), closes [#2864](https://landmarkcu.visualstudio.com///issues/2864)
* **AccountPage:** Allow Non-Loaded Accounts to show on AccountPage ([1915dc0](https://landmarkcu.visualstudio.com///commit/1915dc0)), closes [#2669](https://landmarkcu.visualstudio.com///issues/2669)
* **AccountPage:** Fix Erroneous Loading in AccountPage ([5328674](https://landmarkcu.visualstudio.com///commit/5328674)), closes [#3095](https://landmarkcu.visualstudio.com///issues/3095) [#3109](https://landmarkcu.visualstudio.com///issues/3109)
* **AccountPageTabView:** Unnecessary tab open callback ([4cba028](https://landmarkcu.visualstudio.com///commit/4cba028))
* **Accounts:** Display buttons, account name, collateral correctly ([3978b1f](https://landmarkcu.visualstudio.com///commit/3978b1f)), closes [#2841](https://landmarkcu.visualstudio.com///issues/2841) [#2843](https://landmarkcu.visualstudio.com///issues/2843) [#2844](https://landmarkcu.visualstudio.com///issues/2844)
* **AlertBar:** Remove empty alert boxes ([d0cf36d](https://landmarkcu.visualstudio.com///commit/d0cf36d))
* **AlertBox:** Restrict Alert Box to 5 Rows ([f0af991](https://landmarkcu.visualstudio.com///commit/f0af991)), closes [#2862](https://landmarkcu.visualstudio.com///issues/2862)
* **AuctionAssignment:** Allow Submission of "Car or Light Truck" ([a511fa2](https://landmarkcu.visualstudio.com///commit/a511fa2)), closes [#3070](https://landmarkcu.visualstudio.com///issues/3070)
* **AuctionAssignmentReducer:** Fix merge conflict ([af516f6](https://landmarkcu.visualstudio.com///commit/af516f6))
* **AuctionAssignmentReducer:** Remove log statement ([99fda3b](https://landmarkcu.visualstudio.com///commit/99fda3b))
* **AuctionInfo:** Remove delete button ([312b160](https://landmarkcu.visualstudio.com///commit/312b160))
* **AuctionInfoSidebar:** Fix styling ([898f667](https://landmarkcu.visualstudio.com///commit/898f667))
* **AuctionTab:** Only Show AuctionAssignment Wizard on REPO ([37760e2](https://landmarkcu.visualstudio.com///commit/37760e2)), closes [#2839](https://landmarkcu.visualstudio.com///issues/2839)
* **BigMaps:** Only refresh map on valid prop changes ([ce7e5d2](https://landmarkcu.visualstudio.com///commit/ce7e5d2)), closes [#2838](https://landmarkcu.visualstudio.com///issues/2838)
* **Collateral:** Correct URL for PUT request ([7e7963a](https://landmarkcu.visualstudio.com///commit/7e7963a))
* **CollateralDetails:** Move Year first ([d351cf4](https://landmarkcu.visualstudio.com///commit/d351cf4))
* **Endpoints:** Change endpoint to QA instead of Local ([592f802](https://landmarkcu.visualstudio.com///commit/592f802))
* **FloorPricingParameters:** Add web.config ([bffbf09](https://landmarkcu.visualstudio.com///commit/bffbf09))
* **FloorPricingParams:** Add iisConfig copying in postbuild ([bc516ed](https://landmarkcu.visualstudio.com///commit/bc516ed))
* **FloorPricingParams:** Replace web config with hash routing ([a1ec05c](https://landmarkcu.visualstudio.com///commit/a1ec05c))
* **FloorPricingParams:** Undo index.tsx import ([71d2a81](https://landmarkcu.visualstudio.com///commit/71d2a81))
* **IE11:** Polyfill to allow app to show in IE11 ([1fc66f7](https://landmarkcu.visualstudio.com///commit/1fc66f7))
* **Mapping:** Change bing to use http or https depending on root url ([0458c67](https://landmarkcu.visualstudio.com///commit/0458c67))
* **NavBar:** Hamburger correctly show menus, adjust margins. ([a2590f5](https://landmarkcu.visualstudio.com///commit/a2590f5)), closes [#2845](https://landmarkcu.visualstudio.com///issues/2845)
* **Redeemed:** Disable redeem button when redeemed ([45415b3](https://landmarkcu.visualstudio.com///commit/45415b3))
* **RepoAccount:** Add AlertTypes ([8c3ebc5](https://landmarkcu.visualstudio.com///commit/8c3ebc5))
* **RepoAccount:** Break up large fetch requests ([93c47a8](https://landmarkcu.visualstudio.com///commit/93c47a8))
* **RepoAccountActions:** Update to handle paged response from RepoAccount API ([d6628b2](https://landmarkcu.visualstudio.com///commit/d6628b2))
* **RepoAccountSelector:** *.tes.ts -> *.test.ts ([0d93db5](https://landmarkcu.visualstudio.com///commit/0d93db5))
* **RepoInfoTab:** Cleanup ([ad0198c](https://landmarkcu.visualstudio.com///commit/ad0198c))
* **RepoInfoTab:** Correctly populate null repo type ([c8142f5](https://landmarkcu.visualstudio.com///commit/c8142f5))
* **RepoInfoTab:** Remove unused UI elements ([c4676bc](https://landmarkcu.visualstudio.com///commit/c4676bc))
* **RepoInfoTab:** Update dropdown correctly ([b628af2](https://landmarkcu.visualstudio.com///commit/b628af2))
* **SelectedAccount:** Revert SelectedAccount back to SelectedAccountNo to fix dual state issue ([087d903](https://landmarkcu.visualstudio.com///commit/087d903))
* **VehicleFloorPricingParameters:** Update UI ([8f71291](https://landmarkcu.visualstudio.com///commit/8f71291))
### Features
* **AccountActivity:** Add dummy AccountActivity via TabView to AccountPage ([a66d0d8](https://landmarkcu.visualstudio.com///commit/a66d0d8)), closes [#2358](https://landmarkcu.visualstudio.com///issues/2358)
* **AccountActivity:** Differentiate Actions & Alerts ([8c057bd](https://landmarkcu.visualstudio.com///commit/8c057bd)), closes [#3052](https://landmarkcu.visualstudio.com///issues/3052)
* **AccountList:** Show basic list of accounts on main screen ([c490ff9](https://landmarkcu.visualstudio.com///commit/c490ff9)), closes [#2162](https://landmarkcu.visualstudio.com///issues/2162)
* **AccountNotifications:** Add Alerts/Actions to Activity Tab ([7124f08](https://landmarkcu.visualstudio.com///commit/7124f08)), closes [#2885](https://landmarkcu.visualstudio.com///issues/2885)
* **AccountNotifications:** Allow Alerts to be Dismissed ([a6f582c](https://landmarkcu.visualstudio.com///commit/a6f582c)), closes [#2886](https://landmarkcu.visualstudio.com///issues/2886)
* **AccountPage:** Add basic details to AccountPage ([65a08c0](https://landmarkcu.visualstudio.com///commit/65a08c0)), closes [#2330](https://landmarkcu.visualstudio.com///issues/2330)
* **AccountPage:** Add Repo Status Changes tab ([1930fd9](https://landmarkcu.visualstudio.com///commit/1930fd9))
* **AccountPage:** Move information into the header of the AccountPage ([1effb9a](https://landmarkcu.visualstudio.com///commit/1effb9a)), closes [#2400](https://landmarkcu.visualstudio.com///issues/2400)
* **AccountPage:** Show viable status changes for an account using settings ([b1fec9a](https://landmarkcu.visualstudio.com///commit/b1fec9a)), closes [#2548](https://landmarkcu.visualstudio.com///issues/2548)
* **Admin:** Refactor admin to be more than one page ([d74f398](https://landmarkcu.visualstudio.com///commit/d74f398)), closes [#2741](https://landmarkcu.visualstudio.com///issues/2741)
* **AlertBar:** Add alert groups ([d56843a](https://landmarkcu.visualstudio.com///commit/d56843a))
* **AlertBox:** Display alert count ([84f5914](https://landmarkcu.visualstudio.com///commit/84f5914))
* **AlertBox:** Use new IAlert model ([99ce0db](https://landmarkcu.visualstudio.com///commit/99ce0db))
* **AlertRow:** Show related accounts on click ([8fae0b6](https://landmarkcu.visualstudio.com///commit/8fae0b6))
* **AlertRow:** Show related accounts on click ([168fb0b](https://landmarkcu.visualstudio.com///commit/168fb0b))
* **Alerts:** Add basic alerts along with Redux ([a63f11a](https://landmarkcu.visualstudio.com///commit/a63f11a))
* **Alerts:** Add basic alerts along with Redux ([540ac8c](https://landmarkcu.visualstudio.com///commit/540ac8c))
* **Alerts:** Add Sub-Message to Alerts & Notifications ([03b04c3](https://landmarkcu.visualstudio.com///commit/03b04c3)), closes [#3117](https://landmarkcu.visualstudio.com///issues/3117)
* **AppConfig:** Implement AppConfig ([eb1e1c5](https://landmarkcu.visualstudio.com///commit/eb1e1c5))
* **AppSkeleton:** Build out app skeleton, providing an Agile and Architectural starting point ([310f42c](https://landmarkcu.visualstudio.com///commit/310f42c)), closes [#1487](https://landmarkcu.visualstudio.com///issues/1487)
* **AuctionAssignment:** Add Bing Maps to Auction Assignment ([02d93f8](https://landmarkcu.visualstudio.com///commit/02d93f8)), closes [#2822](https://landmarkcu.visualstudio.com///issues/2822)
* **AuctionAssignment:** Add Default Legal Sale Date ([fb927bc](https://landmarkcu.visualstudio.com///commit/fb927bc)), closes [#2992](https://landmarkcu.visualstudio.com///issues/2992)
* **AuctionAssignment:** Allow Creation of new Auction Assignments ([6c27227](https://landmarkcu.visualstudio.com///commit/6c27227)), closes [#2797](https://landmarkcu.visualstudio.com///issues/2797)
* **AuctionAssignment:** Create Auction Assignment UI ([1d1fb70](https://landmarkcu.visualstudio.com///commit/1d1fb70)), closes [#2742](https://landmarkcu.visualstudio.com///issues/2742)
* **AuctionAssignment:** Restrict Pickup Date to Future ([8aad1b8](https://landmarkcu.visualstudio.com///commit/8aad1b8)), closes [#2870](https://landmarkcu.visualstudio.com///issues/2870)
* **AuctionAssignmentWizard:** Show CollectionsHistory on new AuctionAssignmentWizard ([bb29bce](https://landmarkcu.visualstudio.com///commit/bb29bce)), closes [#2728](https://landmarkcu.visualstudio.com///issues/2728)
* **AuctionCollateralLocation:** Display Auction Collateral Location in AuctionAssignmentWizard ([b24076c](https://landmarkcu.visualstudio.com///commit/b24076c)), closes [#2735](https://landmarkcu.visualstudio.com///issues/2735)
* **AuctionCollateralSelection:** Allow User to Specify Collateral ([5ab0661](https://landmarkcu.visualstudio.com///commit/5ab0661)), closes [#2762](https://landmarkcu.visualstudio.com///issues/2762)
* **AuctionHouses:** Create Admin Auction House Page ([ffc0c72](https://landmarkcu.visualstudio.com///commit/ffc0c72)), closes [#2934](https://landmarkcu.visualstudio.com///issues/2934)
* **AuctionInfo:** Create AuctionInfo screen ([05f11c5](https://landmarkcu.visualstudio.com///commit/05f11c5)), closes [#2512](https://landmarkcu.visualstudio.com///issues/2512)
* **AuctionInfo:** Notify AutoIMS on redeem ([37aae76](https://landmarkcu.visualstudio.com///commit/37aae76))
* **AuctionInfo:** Redeem button changes repo status ([967599e](https://landmarkcu.visualstudio.com///commit/967599e))
* **AuctionInfo:** Refactor redeem button and add test ([93bbbcd](https://landmarkcu.visualstudio.com///commit/93bbbcd))
* **Authorization:** Always pass JWT/Bearer to api ([3b54593](https://landmarkcu.visualstudio.com///commit/3b54593))
* **Collateral:** Mark as retitled ([f3d900b](https://landmarkcu.visualstudio.com///commit/f3d900b))
* **Collateral:** Refresh multiple collateral items ([16745cc](https://landmarkcu.visualstudio.com///commit/16745cc))
* **Collateral:** Refresh single collateral only ([b296ebe](https://landmarkcu.visualstudio.com///commit/b296ebe))
* **CollateralDisplay:** Create CollateralDisplay component. ([4ca0633](https://landmarkcu.visualstudio.com///commit/4ca0633)), closes [#2326](https://landmarkcu.visualstudio.com///issues/2326)
* **CollectionsHistoryTab:** Create CollectionsHistoryTab ([3600e7d](https://landmarkcu.visualstudio.com///commit/3600e7d)), closes [#2674](https://landmarkcu.visualstudio.com///issues/2674)
* **EventAlert:** Replace AlertType with Message string ([58541e3](https://landmarkcu.visualstudio.com///commit/58541e3))
* **EventAlerts:** Show action needed items and basic account list ([0fedaf4](https://landmarkcu.visualstudio.com///commit/0fedaf4)), closes [#1659](https://landmarkcu.visualstudio.com///issues/1659)
* **FloorPricing:** Edit From & To Conditions & Percentage Of ([e930c34](https://landmarkcu.visualstudio.com///commit/e930c34))
* **FloorPricingParameters:** Disallow gaps between from and to conditions ([39c6fdc](https://landmarkcu.visualstudio.com///commit/39c6fdc))
* **Logging:** Add Server Logging from Client ([6e431a8](https://landmarkcu.visualstudio.com///commit/6e431a8)), closes [#2794](https://landmarkcu.visualstudio.com///issues/2794)
* **PostChargedOffHistory:** Create Charged PostChargedOff History Tab ([1822477](https://landmarkcu.visualstudio.com///commit/1822477)), closes [#2650](https://landmarkcu.visualstudio.com///issues/2650)
* **ReactRouter:** Implement ReactRouter into application ([09f74e0](https://landmarkcu.visualstudio.com///commit/09f74e0))
* **RepoAccount:** Shorten account list URL ([c559d50](https://landmarkcu.visualstudio.com///commit/c559d50))
* **RepoAccount:** Update Reducer to Key:Value ([9d302f6](https://landmarkcu.visualstudio.com///commit/9d302f6)), closes [#3044](https://landmarkcu.visualstudio.com///issues/3044)
* **RepoAccountReducer:** Test state updates with retitled collateral ([25405ab](https://landmarkcu.visualstudio.com///commit/25405ab))
* **RepoAccounts:** Add dummy Borrowers & Collateral to RepoAccounts. ([fcdf460](https://landmarkcu.visualstudio.com///commit/fcdf460)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **RepoInfo:** Disabled Disabled Repo Companies ([4df33d4](https://landmarkcu.visualstudio.com///commit/4df33d4)), closes [#2837](https://landmarkcu.visualstudio.com///issues/2837)
* **RepoInfoTab:** Add RepoType selection ([6a45473](https://landmarkcu.visualstudio.com///commit/6a45473))
* **RepoInfoTab:** Add voluntary status to Repo Type ([6ba38ff](https://landmarkcu.visualstudio.com///commit/6ba38ff))
* **RepoInfoTab:** Change Save Icon to Update Button ([114d1f9](https://landmarkcu.visualstudio.com///commit/114d1f9)), closes [#2824](https://landmarkcu.visualstudio.com///issues/2824)
* **RepoInfoTab:** Display and allow user to select Repo Company ([7882852](https://landmarkcu.visualstudio.com///commit/7882852)), closes [#2470](https://landmarkcu.visualstudio.com///issues/2470)
* **RepoInfoTab:** Notify title sent ([d8d0eb9](https://landmarkcu.visualstudio.com///commit/d8d0eb9))
* **RepoLettersTable:** Initial commit ([ef298e7](https://landmarkcu.visualstudio.com///commit/ef298e7))
* **RepoLettersTable:** Update to QA API URL ([81ff7b3](https://landmarkcu.visualstudio.com///commit/81ff7b3))
* **Search:** Implement Search ([56f5bf8](https://landmarkcu.visualstudio.com///commit/56f5bf8)), closes [#3087](https://landmarkcu.visualstudio.com///issues/3087)
* **Security:** Require users to member of AD to see app. ([cf1d5ab](https://landmarkcu.visualstudio.com///commit/cf1d5ab)), closes [#1723](https://landmarkcu.visualstudio.com///issues/1723) [#1724](https://landmarkcu.visualstudio.com///issues/1724)
* **Security:** Restrict user for accessing app if they don't have role(s) ([25f6cca](https://landmarkcu.visualstudio.com///commit/25f6cca)), closes [#1723](https://landmarkcu.visualstudio.com///issues/1723) [#1724](https://landmarkcu.visualstudio.com///issues/1724)
* **Setup:** Initial React App package.json setup ([926c7b7](https://landmarkcu.visualstudio.com///commit/926c7b7))
* **SoldBanner:** Add Sold Banner to Auction Assignments ([ecb6ca7](https://landmarkcu.visualstudio.com///commit/ecb6ca7)), closes [#2942](https://landmarkcu.visualstudio.com///issues/2942)
* **StatusChange:** Status Change Now Posts Update ([ac0d7c5](https://landmarkcu.visualstudio.com///commit/ac0d7c5)), closes [#2842](https://landmarkcu.visualstudio.com///issues/2842)
* **TabView:** Create TabView Component ([7283796](https://landmarkcu.visualstudio.com///commit/7283796)), closes [#2354](https://landmarkcu.visualstudio.com///issues/2354)
* **Toasts:** Allow actions to toast the user with information. ([bfd9ddd](https://landmarkcu.visualstudio.com///commit/bfd9ddd)), closes [#2715](https://landmarkcu.visualstudio.com///issues/2715)
* **Toasts:** Implement HTTP Failure Toasts ([be6da99](https://landmarkcu.visualstudio.com///commit/be6da99)), closes [#2793](https://landmarkcu.visualstudio.com///issues/2793)
* **TransactionHistoryTab:** Implement Transaction History Tab ([04601e6](https://landmarkcu.visualstudio.com///commit/04601e6)), closes [#2684](https://landmarkcu.visualstudio.com///issues/2684)
* **VehicleFloorPricingParams:** Add component ([997aa77](https://landmarkcu.visualstudio.com///commit/997aa77))
* **Versioning:** Version app, fix up footer, header, and configuration. ([6f7a0bb](https://landmarkcu.visualstudio.com///commit/6f7a0bb)), closes [#2716](https://landmarkcu.visualstudio.com///issues/2716)
* **WrittenOffField:** Add Written Off Field to AccountDetails ([2400ed0](https://landmarkcu.visualstudio.com///commit/2400ed0)), closes [#2698](https://landmarkcu.visualstudio.com///issues/2698)
* Implement view transitions between StartPage and AccountPage ([54e5f85](https://landmarkcu.visualstudio.com///commit/54e5f85)), closes [#2321](https://landmarkcu.visualstudio.com///issues/2321)
### Tests
* **AccountDetails:** Add tests to AccountDetails ([44f2466](https://landmarkcu.visualstudio.com///commit/44f2466))
* **AccountPage:** Add tests for getViableStatuses ([02c82cc](https://landmarkcu.visualstudio.com///commit/02c82cc))
* **AlertBox:** Add rendering tests ([29c8a96](https://landmarkcu.visualstudio.com///commit/29c8a96))
* **AlertReducer:** Add unit test ([805d3a6](https://landmarkcu.visualstudio.com///commit/805d3a6))
* **Client:** Add Tests Across Various Components ([41f7e7b](https://landmarkcu.visualstudio.com///commit/41f7e7b))
* **CollateralDisplay:** Fix broken test cases for CollateralDisplay ([28ec130](https://landmarkcu.visualstudio.com///commit/28ec130)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **EventAlert:** EventAlert integration use mock store ([8c9a284](https://landmarkcu.visualstudio.com///commit/8c9a284))
* **EventAlert:** Update event alert tests with real store ([94640b0](https://landmarkcu.visualstudio.com///commit/94640b0))
* **RepoInfoTab:** Implement tests for RepoInfoTab ([c9d074d](https://landmarkcu.visualstudio.com///commit/c9d074d)), closes [#2451](https://landmarkcu.visualstudio.com///issues/2451)
* **SoldBanner:** Add Tests to Sold Banner ([6c38b0c](https://landmarkcu.visualstudio.com///commit/6c38b0c)), closes [#2942](https://landmarkcu.visualstudio.com///issues/2942)
## 0.3.0 (2019-08-29)
### Bug Fixes
* **AccountPage:** Allow Non-Loaded Accounts to show on AccountPage ([1915dc0](https://landmarkcu.visualstudio.com///commit/1915dc0)), closes [#2669](https://landmarkcu.visualstudio.com///issues/2669)
* **Accounts:** Display buttons, account name, collateral correctly ([3978b1f](https://landmarkcu.visualstudio.com///commit/3978b1f)), closes [#2841](https://landmarkcu.visualstudio.com///issues/2841) [#2843](https://landmarkcu.visualstudio.com///issues/2843) [#2844](https://landmarkcu.visualstudio.com///issues/2844)
* **AlertBar:** Remove empty alert boxes ([d0cf36d](https://landmarkcu.visualstudio.com///commit/d0cf36d))
* **AuctionInfo:** Remove delete button ([312b160](https://landmarkcu.visualstudio.com///commit/312b160))
* **AuctionInfoSidebar:** Fix styling ([898f667](https://landmarkcu.visualstudio.com///commit/898f667))
* **AuctionTab:** Only Show AuctionAssignment Wizard on REPO ([37760e2](https://landmarkcu.visualstudio.com///commit/37760e2)), closes [#2839](https://landmarkcu.visualstudio.com///issues/2839)
* **BigMaps:** Only refresh map on valid prop changes ([ce7e5d2](https://landmarkcu.visualstudio.com///commit/ce7e5d2)), closes [#2838](https://landmarkcu.visualstudio.com///issues/2838)
* **Collateral:** Correct URL for PUT request ([7e7963a](https://landmarkcu.visualstudio.com///commit/7e7963a))
* **CollateralDetails:** Move Year first ([d351cf4](https://landmarkcu.visualstudio.com///commit/d351cf4))
* **Endpoints:** Change endpoint to QA instead of Local ([592f802](https://landmarkcu.visualstudio.com///commit/592f802))
* **FloorPricingParameters:** Add web.config ([bffbf09](https://landmarkcu.visualstudio.com///commit/bffbf09))
* **FloorPricingParams:** Add iisConfig copying in postbuild ([bc516ed](https://landmarkcu.visualstudio.com///commit/bc516ed))
* **FloorPricingParams:** Replace web config with hash routing ([a1ec05c](https://landmarkcu.visualstudio.com///commit/a1ec05c))
* **FloorPricingParams:** Undo index.tsx import ([71d2a81](https://landmarkcu.visualstudio.com///commit/71d2a81))
* **Mapping:** Change bing to use http or https depending on root url ([0458c67](https://landmarkcu.visualstudio.com///commit/0458c67))
* **NavBar:** Hamburger correctly show menus, adjust margins. ([a2590f5](https://landmarkcu.visualstudio.com///commit/a2590f5)), closes [#2845](https://landmarkcu.visualstudio.com///issues/2845)
* **RepoAccount:** Add AlertTypes ([8c3ebc5](https://landmarkcu.visualstudio.com///commit/8c3ebc5))
* **RepoAccount:** Break up large fetch requests ([93c47a8](https://landmarkcu.visualstudio.com///commit/93c47a8))
* **RepoAccountActions:** Update to handle paged response from RepoAccount API ([d6628b2](https://landmarkcu.visualstudio.com///commit/d6628b2))
* **RepoInfoTab:** Cleanup ([ad0198c](https://landmarkcu.visualstudio.com///commit/ad0198c))
* **RepoInfoTab:** Cleanup ([c1d5d0a](https://landmarkcu.visualstudio.com///commit/c1d5d0a))
* **RepoInfoTab:** Correctly populate null repo type ([c8142f5](https://landmarkcu.visualstudio.com///commit/c8142f5))
* **RepoInfoTab:** Remove unused UI elements ([c4676bc](https://landmarkcu.visualstudio.com///commit/c4676bc))
* **RepoInfoTab:** Update dropdown correctly ([b628af2](https://landmarkcu.visualstudio.com///commit/b628af2))
* **SelectedAccount:** Revert SelectedAccount back to SelectedAccountNo to fix dual state issue ([087d903](https://landmarkcu.visualstudio.com///commit/087d903))
### Features
* **AccountActivity:** Add dummy AccountActivity via TabView to AccountPage ([a66d0d8](https://landmarkcu.visualstudio.com///commit/a66d0d8)), closes [#2358](https://landmarkcu.visualstudio.com///issues/2358)
* **AccountList:** Show basic list of accounts on main screen ([c490ff9](https://landmarkcu.visualstudio.com///commit/c490ff9)), closes [#2162](https://landmarkcu.visualstudio.com///issues/2162)
* **AccountPage:** Add basic details to AccountPage ([65a08c0](https://landmarkcu.visualstudio.com///commit/65a08c0)), closes [#2330](https://landmarkcu.visualstudio.com///issues/2330)
* **AccountPage:** Move information into the header of the AccountPage ([1effb9a](https://landmarkcu.visualstudio.com///commit/1effb9a)), closes [#2400](https://landmarkcu.visualstudio.com///issues/2400)
* **AccountPage:** Show viable status changes for an account using settings ([b1fec9a](https://landmarkcu.visualstudio.com///commit/b1fec9a)), closes [#2548](https://landmarkcu.visualstudio.com///issues/2548)
* **Admin:** Refactor admin to be more than one page ([d74f398](https://landmarkcu.visualstudio.com///commit/d74f398)), closes [#2741](https://landmarkcu.visualstudio.com///issues/2741)
* **AlertBar:** Add alert groups ([d56843a](https://landmarkcu.visualstudio.com///commit/d56843a))
* **AlertBox:** Display alert count ([84f5914](https://landmarkcu.visualstudio.com///commit/84f5914))
* **AlertBox:** Use new IAlert model ([99ce0db](https://landmarkcu.visualstudio.com///commit/99ce0db))
* **AlertRow:** Show related accounts on click ([8fae0b6](https://landmarkcu.visualstudio.com///commit/8fae0b6))
* **Alerts:** Add basic alerts along with Redux ([540ac8c](https://landmarkcu.visualstudio.com///commit/540ac8c))
* **AppConfig:** Implement AppConfig ([eb1e1c5](https://landmarkcu.visualstudio.com///commit/eb1e1c5))
* **AppSkeleton:** Build out app skeleton, providing an Agile and Architectural starting point ([310f42c](https://landmarkcu.visualstudio.com///commit/310f42c)), closes [#1487](https://landmarkcu.visualstudio.com///issues/1487)
* **AuctionAssignment:** Add Bing Maps to Auction Assignment ([02d93f8](https://landmarkcu.visualstudio.com///commit/02d93f8)), closes [#2822](https://landmarkcu.visualstudio.com///issues/2822)
* **AuctionAssignment:** Allow Creation of new Auction Assignments ([6c27227](https://landmarkcu.visualstudio.com///commit/6c27227)), closes [#2797](https://landmarkcu.visualstudio.com///issues/2797)
* **AuctionAssignment:** Create Auction Assignment UI ([1d1fb70](https://landmarkcu.visualstudio.com///commit/1d1fb70)), closes [#2742](https://landmarkcu.visualstudio.com///issues/2742)
* **AuctionAssignmentWizard:** Show CollectionsHistory on new AuctionAssignmentWizard ([bb29bce](https://landmarkcu.visualstudio.com///commit/bb29bce)), closes [#2728](https://landmarkcu.visualstudio.com///issues/2728)
* **AuctionCollateralLocation:** Display Auction Collateral Location in AuctionAssignmentWizard ([b24076c](https://landmarkcu.visualstudio.com///commit/b24076c)), closes [#2735](https://landmarkcu.visualstudio.com///issues/2735)
* **AuctionCollateralSelection:** Allow User to Specify Collateral ([5ab0661](https://landmarkcu.visualstudio.com///commit/5ab0661)), closes [#2762](https://landmarkcu.visualstudio.com///issues/2762)
* **AuctionInfo:** Create AuctionInfo screen ([05f11c5](https://landmarkcu.visualstudio.com///commit/05f11c5)), closes [#2512](https://landmarkcu.visualstudio.com///issues/2512)
* **AuctionInfo:** Notify AutoIMS on redeem ([37aae76](https://landmarkcu.visualstudio.com///commit/37aae76))
* **AuctionInfo:** Redeem button changes repo status ([967599e](https://landmarkcu.visualstudio.com///commit/967599e))
* **AuctionInfo:** Refactor redeem button and add test ([93bbbcd](https://landmarkcu.visualstudio.com///commit/93bbbcd))
* **Authorization:** Always pass JWT/Bearer to api ([3b54593](https://landmarkcu.visualstudio.com///commit/3b54593))
* **Collateral:** Mark as retitled ([f3d900b](https://landmarkcu.visualstudio.com///commit/f3d900b))
* **Collateral:** Refresh multiple collateral items ([16745cc](https://landmarkcu.visualstudio.com///commit/16745cc))
* **Collateral:** Refresh single collateral only ([b296ebe](https://landmarkcu.visualstudio.com///commit/b296ebe))
* **CollateralDisplay:** Create CollateralDisplay component. ([4ca0633](https://landmarkcu.visualstudio.com///commit/4ca0633)), closes [#2326](https://landmarkcu.visualstudio.com///issues/2326)
* **CollectionsHistoryTab:** Create CollectionsHistoryTab ([3600e7d](https://landmarkcu.visualstudio.com///commit/3600e7d)), closes [#2674](https://landmarkcu.visualstudio.com///issues/2674)
* **EventAlerts:** Show action needed items and basic account list ([0fedaf4](https://landmarkcu.visualstudio.com///commit/0fedaf4)), closes [#1659](https://landmarkcu.visualstudio.com///issues/1659)
* **Logging:** Add Server Logging from Client ([6e431a8](https://landmarkcu.visualstudio.com///commit/6e431a8)), closes [#2794](https://landmarkcu.visualstudio.com///issues/2794)
* **PostChargedOffHistory:** Create Charged PostChargedOff History Tab ([1822477](https://landmarkcu.visualstudio.com///commit/1822477)), closes [#2650](https://landmarkcu.visualstudio.com///issues/2650)
* **ReactRouter:** Implement ReactRouter into application ([09f74e0](https://landmarkcu.visualstudio.com///commit/09f74e0))
* **RepoAccount:** Shorten account list URL ([c559d50](https://landmarkcu.visualstudio.com///commit/c559d50))
* **RepoAccountReducer:** Test state updates with retitled collateral ([25405ab](https://landmarkcu.visualstudio.com///commit/25405ab))
* **RepoAccounts:** Add dummy Borrowers & Collateral to RepoAccounts. ([fcdf460](https://landmarkcu.visualstudio.com///commit/fcdf460)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **RepoInfoTab:** Add RepoType selection ([6a45473](https://landmarkcu.visualstudio.com///commit/6a45473))
* **RepoInfoTab:** Add voluntary status to Repo Type ([6ba38ff](https://landmarkcu.visualstudio.com///commit/6ba38ff))
* **RepoInfoTab:** Change Save Icon to Update Button ([114d1f9](https://landmarkcu.visualstudio.com///commit/114d1f9)), closes [#2824](https://landmarkcu.visualstudio.com///issues/2824)
* **RepoInfoTab:** Display and allow user to select Repo Company ([7882852](https://landmarkcu.visualstudio.com///commit/7882852)), closes [#2470](https://landmarkcu.visualstudio.com///issues/2470)
* **RepoInfoTab:** Notify title sent ([d8d0eb9](https://landmarkcu.visualstudio.com///commit/d8d0eb9))
* **Security:** Require users to member of AD to see app. ([cf1d5ab](https://landmarkcu.visualstudio.com///commit/cf1d5ab)), closes [#1723](https://landmarkcu.visualstudio.com///issues/1723) [#1724](https://landmarkcu.visualstudio.com///issues/1724)
* **Security:** Restrict user for accessing app if they don't have role(s) ([25f6cca](https://landmarkcu.visualstudio.com///commit/25f6cca)), closes [#1723](https://landmarkcu.visualstudio.com///issues/1723) [#1724](https://landmarkcu.visualstudio.com///issues/1724)
* **Setup:** Initial React App package.json setup ([926c7b7](https://landmarkcu.visualstudio.com///commit/926c7b7))
* **StatusChange:** Status Change Now Posts Update ([ac0d7c5](https://landmarkcu.visualstudio.com///commit/ac0d7c5)), closes [#2842](https://landmarkcu.visualstudio.com///issues/2842)
* **TabView:** Create TabView Component ([7283796](https://landmarkcu.visualstudio.com///commit/7283796)), closes [#2354](https://landmarkcu.visualstudio.com///issues/2354)
* **Toasts:** Allow actions to toast the user with information. ([bfd9ddd](https://landmarkcu.visualstudio.com///commit/bfd9ddd)), closes [#2715](https://landmarkcu.visualstudio.com///issues/2715)
* **Toasts:** Implement HTTP Failure Toasts ([be6da99](https://landmarkcu.visualstudio.com///commit/be6da99)), closes [#2793](https://landmarkcu.visualstudio.com///issues/2793)
* **TransactionHistoryTab:** Implement Transaction History Tab ([04601e6](https://landmarkcu.visualstudio.com///commit/04601e6)), closes [#2684](https://landmarkcu.visualstudio.com///issues/2684)
* **VehicleFloorPricingParams:** Add component ([997aa77](https://landmarkcu.visualstudio.com///commit/997aa77))
* **Versioning:** Version app, fix up footer, header, and configuration. ([6f7a0bb](https://landmarkcu.visualstudio.com///commit/6f7a0bb)), closes [#2716](https://landmarkcu.visualstudio.com///issues/2716)
* **WrittenOffField:** Add Written Off Field to AccountDetails ([cc9f514](https://landmarkcu.visualstudio.com///commit/cc9f514)), closes [#2698](https://landmarkcu.visualstudio.com///issues/2698)
* **No Topic:** Implement view transitions between StartPage and AccountPage ([54e5f85](https://landmarkcu.visualstudio.com///commit/54e5f85)), closes [#2321](https://landmarkcu.visualstudio.com///issues/2321)
### Tests
* **AccountDetails:** Add tests to AccountDetails ([44f2466](https://landmarkcu.visualstudio.com///commit/44f2466))
* **AccountPage:** Add tests for getViableStatuses ([02c82cc](https://landmarkcu.visualstudio.com///commit/02c82cc))
* **AlertBox:** Add rendering tests ([29c8a96](https://landmarkcu.visualstudio.com///commit/29c8a96))
* **AlertReducer:** Add unit test ([805d3a6](https://landmarkcu.visualstudio.com///commit/805d3a6))
* **Client:** Add Tests Across Various Components ([41f7e7b](https://landmarkcu.visualstudio.com///commit/41f7e7b))
* **CollateralDisplay:** Fix broken test cases for CollateralDisplay ([28ec130](https://landmarkcu.visualstudio.com///commit/28ec130)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **EventAlert:** EventAlert integration use mock store ([8c9a284](https://landmarkcu.visualstudio.com///commit/8c9a284))
* **EventAlert:** Update event alert tests with real store ([94640b0](https://landmarkcu.visualstudio.com///commit/94640b0))
* **RepoInfoTab:** Implement tests for RepoInfoTab ([c9d074d](https://landmarkcu.visualstudio.com///commit/c9d074d)), closes [#2451](https://landmarkcu.visualstudio.com///issues/2451)
## 0.2.0 (2019-08-22)
### Bug Fixes
* **AccountPage:** Allow Non-Loaded Accounts to show on AccountPage ([1915dc0](https://landmarkcu.visualstudio.com///commit/1915dc0)), closes [#2669](https://landmarkcu.visualstudio.com///issues/2669)
* **AlertBar:** Remove empty alert boxes ([d0cf36d](https://landmarkcu.visualstudio.com///commit/d0cf36d))
* **AuctionAssignmentReducer:** Fix merge conflict ([af516f6](https://landmarkcu.visualstudio.com///commit/af516f6))
* **AuctionAssignmentReducer:** Remove log statement ([99fda3b](https://landmarkcu.visualstudio.com///commit/99fda3b))
* **AuctionInfo:** Remove delete button ([312b160](https://landmarkcu.visualstudio.com///commit/312b160))
* **AuctionInfoSidebar:** Fix styling ([898f667](https://landmarkcu.visualstudio.com///commit/898f667))
* **Collateral:** Correct URL for PUT request ([7e7963a](https://landmarkcu.visualstudio.com///commit/7e7963a))
* **CollateralDetails:** Move Year first ([d351cf4](https://landmarkcu.visualstudio.com///commit/d351cf4))
* **Endpoints:** Change endpoint to QA instead of Local ([592f802](https://landmarkcu.visualstudio.com///commit/592f802))
* **FloorPricingParameters:** Add web.config ([bffbf09](https://landmarkcu.visualstudio.com///commit/bffbf09))
* **FloorPricingParams:** Add iisConfig copying in postbuild ([bc516ed](https://landmarkcu.visualstudio.com///commit/bc516ed))
* **FloorPricingParams:** Replace web config with hash routing ([a1ec05c](https://landmarkcu.visualstudio.com///commit/a1ec05c))
* **FloorPricingParams:** Undo index.tsx import ([71d2a81](https://landmarkcu.visualstudio.com///commit/71d2a81))
* **RepoAccount:** Add AlertTypes ([8c3ebc5](https://landmarkcu.visualstudio.com///commit/8c3ebc5))
* **RepoAccount:** Break up large fetch requests ([93c47a8](https://landmarkcu.visualstudio.com///commit/93c47a8))
* **RepoAccountActions:** Update to handle paged response from RepoAccount API ([d6628b2](https://landmarkcu.visualstudio.com///commit/d6628b2))
* **RepoInfoTab:** Cleanup ([ad0198c](https://landmarkcu.visualstudio.com///commit/ad0198c))
* **RepoInfoTab:** Cleanup ([c1d5d0a](https://landmarkcu.visualstudio.com///commit/c1d5d0a))
* **RepoInfoTab:** Correctly populate null repo type ([c8142f5](https://landmarkcu.visualstudio.com///commit/c8142f5))
* **RepoInfoTab:** Remove unused UI elements ([c4676bc](https://landmarkcu.visualstudio.com///commit/c4676bc))
* **RepoInfoTab:** Update dropdown correctly ([b628af2](https://landmarkcu.visualstudio.com///commit/b628af2))
* **SelectedAccount:** Revert SelectedAccount back to SelectedAccountNo to fix dual state issue ([087d903](https://landmarkcu.visualstudio.com///commit/087d903))
### Features
* **AccountActivity:** Add dummy AccountActivity via TabView to AccountPage ([a66d0d8](https://landmarkcu.visualstudio.com///commit/a66d0d8)), closes [#2358](https://landmarkcu.visualstudio.com///issues/2358)
* **AccountList:** Show basic list of accounts on main screen ([c490ff9](https://landmarkcu.visualstudio.com///commit/c490ff9)), closes [#2162](https://landmarkcu.visualstudio.com///issues/2162)
* **AccountPage:** Add basic details to AccountPage ([65a08c0](https://landmarkcu.visualstudio.com///commit/65a08c0)), closes [#2330](https://landmarkcu.visualstudio.com///issues/2330)
* **AccountPage:** Move information into the header of the AccountPage ([1effb9a](https://landmarkcu.visualstudio.com///commit/1effb9a)), closes [#2400](https://landmarkcu.visualstudio.com///issues/2400)
* **AccountPage:** Show viable status changes for an account using settings ([b1fec9a](https://landmarkcu.visualstudio.com///commit/b1fec9a)), closes [#2548](https://landmarkcu.visualstudio.com///issues/2548)
* **AlertBar:** Add alert groups ([d56843a](https://landmarkcu.visualstudio.com///commit/d56843a))
* **AlertBox:** Display alert count ([84f5914](https://landmarkcu.visualstudio.com///commit/84f5914))
* **AlertBox:** Use new IAlert model ([99ce0db](https://landmarkcu.visualstudio.com///commit/99ce0db))
* **AlertRow:** Show related accounts on click ([8fae0b6](https://landmarkcu.visualstudio.com///commit/8fae0b6))
* **AlertRow:** Show related accounts on click ([168fb0b](https://landmarkcu.visualstudio.com///commit/168fb0b))
* **Alerts:** Add basic alerts along with Redux ([540ac8c](https://landmarkcu.visualstudio.com///commit/540ac8c))
* **Alerts:** Add basic alerts along with Redux ([a63f11a](https://landmarkcu.visualstudio.com///commit/a63f11a))
* **AppConfig:** Implement AppConfig ([eb1e1c5](https://landmarkcu.visualstudio.com///commit/eb1e1c5))
* **AppSkeleton:** Build out app skeleton, providing an Agile and Architectural starting point ([310f42c](https://landmarkcu.visualstudio.com///commit/310f42c)), closes [#1487](https://landmarkcu.visualstudio.com///issues/1487)
* **AuctionAssignment:** Create Auction Assignment UI ([1d1fb70](https://landmarkcu.visualstudio.com///commit/1d1fb70)), closes [#2742](https://landmarkcu.visualstudio.com///issues/2742)
* **AuctionAssignmentWizard:** Show CollectionsHistory on new AuctionAssignmentWizard ([bb29bce](https://landmarkcu.visualstudio.com///commit/bb29bce)), closes [#2728](https://landmarkcu.visualstudio.com///issues/2728)
* **AuctionCollateralLocation:** Display Auction Collateral Location in AuctionAssignmentWizard ([b24076c](https://landmarkcu.visualstudio.com///commit/b24076c)), closes [#2735](https://landmarkcu.visualstudio.com///issues/2735)
* **AuctionCollateralSelection:** Allow User to Specify Collateral ([5ab0661](https://landmarkcu.visualstudio.com///commit/5ab0661)), closes [#2762](https://landmarkcu.visualstudio.com///issues/2762)
* **AuctionInfo:** Create AuctionInfo screen ([c356ff0](https://landmarkcu.visualstudio.com///commit/c356ff0)), closes [#2512](https://landmarkcu.visualstudio.com///issues/2512)
* **AuctionInfo:** Create AuctionInfo screen ([05f11c5](https://landmarkcu.visualstudio.com///commit/05f11c5)), closes [#2512](https://landmarkcu.visualstudio.com///issues/2512)
* **AuctionInfo:** Redeem button changes repo status ([967599e](https://landmarkcu.visualstudio.com///commit/967599e))
* **AuctionInfo:** Refactor redeem button and add test ([93bbbcd](https://landmarkcu.visualstudio.com///commit/93bbbcd))
* **Collateral:** Mark as retitled ([f3d900b](https://landmarkcu.visualstudio.com///commit/f3d900b))
* **Collateral:** Refresh multiple collateral items ([16745cc](https://landmarkcu.visualstudio.com///commit/16745cc))
* **Collateral:** Refresh single collateral only ([b296ebe](https://landmarkcu.visualstudio.com///commit/b296ebe))
* **CollateralDisplay:** Create CollateralDisplay component. ([4ca0633](https://landmarkcu.visualstudio.com///commit/4ca0633)), closes [#2326](https://landmarkcu.visualstudio.com///issues/2326)
* **CollectionsHistoryTab:** Create CollectionsHistoryTab ([3600e7d](https://landmarkcu.visualstudio.com///commit/3600e7d)), closes [#2674](https://landmarkcu.visualstudio.com///issues/2674)
* **EventAlerts:** Show action needed items and basic account list ([0fedaf4](https://landmarkcu.visualstudio.com///commit/0fedaf4)), closes [#1659](https://landmarkcu.visualstudio.com///issues/1659)
* **PostChargedOffHistory:** Create Charged PostChargedOff History Tab ([1822477](https://landmarkcu.visualstudio.com///commit/1822477)), closes [#2650](https://landmarkcu.visualstudio.com///issues/2650)
* **ReactRouter:** Implement ReactRouter into application ([09f74e0](https://landmarkcu.visualstudio.com///commit/09f74e0))
* **RepoAccount:** Shorten account list URL ([c559d50](https://landmarkcu.visualstudio.com///commit/c559d50))
* **RepoAccountReducer:** Test state updates with retitled collateral ([25405ab](https://landmarkcu.visualstudio.com///commit/25405ab))
* **RepoAccounts:** Add dummy Borrowers & Collateral to RepoAccounts. ([fcdf460](https://landmarkcu.visualstudio.com///commit/fcdf460)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **RepoInfoTab:** Add RepoType selection ([6a45473](https://landmarkcu.visualstudio.com///commit/6a45473))
* **RepoInfoTab:** Add voluntary status to Repo Type ([6ba38ff](https://landmarkcu.visualstudio.com///commit/6ba38ff))
* **RepoInfoTab:** Display and allow user to select Repo Company ([7882852](https://landmarkcu.visualstudio.com///commit/7882852)), closes [#2470](https://landmarkcu.visualstudio.com///issues/2470)
* **RepoInfoTab:** Notify title sent ([d8d0eb9](https://landmarkcu.visualstudio.com///commit/d8d0eb9))
* **Setup:** Initial React App package.json setup ([926c7b7](https://landmarkcu.visualstudio.com///commit/926c7b7))
* **TabView:** Create TabView Component ([7283796](https://landmarkcu.visualstudio.com///commit/7283796)), closes [#2354](https://landmarkcu.visualstudio.com///issues/2354)
* **TransactionHistoryTab:** Implement Transaction History Tab ([04601e6](https://landmarkcu.visualstudio.com///commit/04601e6)), closes [#2684](https://landmarkcu.visualstudio.com///issues/2684)
* **VehicleFloorPricingParams:** Add component ([997aa77](https://landmarkcu.visualstudio.com///commit/997aa77))
* **WrittenOffField:** Add Written Off Field to AccountDetails ([2400ed0](https://landmarkcu.visualstudio.com///commit/2400ed0)), closes [#2698](https://landmarkcu.visualstudio.com///issues/2698)
* **WrittenOffField:** Add Written Off Field to AccountDetails ([cc9f514](https://landmarkcu.visualstudio.com///commit/cc9f514)), closes [#2698](https://landmarkcu.visualstudio.com///issues/2698)
* Implement view transitions between StartPage and AccountPage ([54e5f85](https://landmarkcu.visualstudio.com///commit/54e5f85)), closes [#2321](https://landmarkcu.visualstudio.com///issues/2321)
### Tests
* **AccountDetails:** Add tests to AccountDetails ([44f2466](https://landmarkcu.visualstudio.com///commit/44f2466))
* **AccountDetails:** Add tests to AccountDetails ([101f32f](https://landmarkcu.visualstudio.com///commit/101f32f))
* **AccountPage:** Add tests for getViableStatuses ([02c82cc](https://landmarkcu.visualstudio.com///commit/02c82cc))
* **AlertBox:** Add rendering tests ([29c8a96](https://landmarkcu.visualstudio.com///commit/29c8a96))
* **AlertReducer:** Add unit test ([805d3a6](https://landmarkcu.visualstudio.com///commit/805d3a6))
* **Client:** Add Tests Across Various Components ([41f7e7b](https://landmarkcu.visualstudio.com///commit/41f7e7b))
* **Client:** Add Tests Across Various Components ([102722b](https://landmarkcu.visualstudio.com///commit/102722b))
* **CollateralDisplay:** Fix broken test cases for CollateralDisplay ([28ec130](https://landmarkcu.visualstudio.com///commit/28ec130)), closes [#2350](https://landmarkcu.visualstudio.com///issues/2350)
* **EventAlert:** EventAlert integration use mock store ([8c9a284](https://landmarkcu.visualstudio.com///commit/8c9a284))
* **EventAlert:** Update event alert tests with real store ([94640b0](https://landmarkcu.visualstudio.com///commit/94640b0))
* **RepoInfoTab:** Implement tests for RepoInfoTab ([c9d074d](https://landmarkcu.visualstudio.com///commit/c9d074d)), closes [#2451](https://landmarkcu.visualstudio.com///issues/2451)
@@ -0,0 +1,254 @@
# Landmark Credit Union Repo Application
This readme outlines the developer process, basic coding style guide and provides guidance and details around the technical approach to the user interface. You should always review and update this document to encompass your changes.
# Getting Started
Before you can work with the code, build it, or deploy it you must setup your development environment. The required environment is Git, Node, and something to work with Typescript.
This environment code has been wrapped up into a Powershell script that should run on any Windows 10 machine and produce a viable development environment to carry you through this guide.
The script is available in the root of this directory as `setup-env.ps1`.
You should start by reviewing and possibly running that script if needed.
## Working With The Code
Code is checked out from the Azure DevOps Repository, which is currently [https://landmarkcu.visualstudio.com/Repo/_git/LCU.WebApps.Collections.RepoClient] with the back end code
located at [https://landmarkcu.visualstudio.com/Repo/_git/LCU.API.Collections.RepoApi].
Work should be done in a branch from Develop. In the shared folder there is a software development process document that talks about this, you should start there. A brief introduction will be included here.
Start by checking out the repository, and specifically the develop branch. You should be branching off of develop
with your own working branch. When you are complete, you will create a pull request within Azure DevOps here: [https://landmarkcu.visualstudio.com/Repo/_git/LCU.WebApps.Collections.RepoClient/pullrequests]
Before submitting your pull request, review the following:
* Did you commit with commitizen? `git cz` (see below)
* Did you add all needed unit testing, and ensure all existing tests are passing?
* Have you run the e2e tests and ensured they all pass?
In order to work and submit code, the project will need to be built and run. Instructions follow. Generally the project
doesn't need to be built, just run locally. However, running a production build after major changes will ensure the
production build is still working before CI runs.
Finally, you'll need to know React, Redux and Typescript.
# Learning React, Redux, Typescript, etc
### Docs:
React, Redux And Typescript have great quick-start guides that walk through their features step by step with examples:
* React: [https://reactjs.org/docs/getting-started.html](https://reactjs.org/docs/getting-started.html)
* Redux: [https://redux.js.org/introduction/core-concepts](https://redux.js.org/introduction/core-concepts)
* Typescript: [https://www.typescriptlang.org/docs/home.html](https://www.typescriptlang.org/docs/home.html)
### Useful Resources:
~~1. https://medium.com/@tkssharma/understanding-redux-react-in-easiest-way-part-1-81f3209fc0e5~~
2. https://blog.usejournal.com/using-react-with-redux-and-typescript-c7ec48c211f6
3. https://hackernoon.com/redux-step-by-step-a-simple-and-robust-workflow-for-real-life-apps-1fdf7df46092
4. https://medium.com/naukri-engineering/email-sending-architecture-using-messaging-queue-314a18f8595c
5. https://reactjs.org/docs/higher-order-components.html#convention-maximizing-composability
6. https://blog.logrocket.com/react-redux-connect-when-and-how-to-use-it-f2a1edab2013/
7. https://stackoverflow.com/questions/54154673/react-typescript-redux-connect-function-not-working
8. https://redux.js.org/recipes/structuring-reducers/basic-reducer-structure
# Redux in TypeScript Quick Guide
1. Know what a Higher Order Component is.
2. The types: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react-redux/index.d.ts#L59
3. Connect is a higher order component used to wrap your component giving it access to state from the store and dispatch actions.
### Initial Install Dependencies
Remove node_modules directory if exists.
Remove .npmrc file.
Inside `package.json` remove dependency `"lcu-react-lib": "^2.18.0"`
Run `npm install`
Inside `package.json` run the `"refreshVSToken"` script.
Inside `package.json` add back dependency `"lcu-react-lib": "^2.18.0"`
Run `npm install`
From the source control tab of VS Code, Discard changes on the .npmrc file to bring it back into the project code
### Build
Run `npm run build` to build the project with default settings, which for create-react-app is production. The development
server will do a debug build.
### Run (Development server)
Run `npm start` for a dev server. Navigate to `https://localhost:3000/`. The app will automatically reload if you change any of the source files.
Note that this will run with a proxy in place so that you can contact the web services.
### Deploy (QA, UAT, Prod)
Deployments to QA, UAT, and Prod are automatically handled by Azure Dev-Ops. See the additional resources section below; as there is a document outlining the software development process, as well as the cloud setup Azure.
### Running unit tests
Run `npm run test` to run the unit tests interactively, they will re-run whenever you make changes. To simulate
the CI server and run them once, `npm run test-ci` or set the CI environment variable before running `npm run test`
### Resources
There is additional documentation available within the Landmark shared folders; you should check the files tab under
the RepoDB team in Microsoft Teams:
* [Architecture Document: Landmark Credit Union - Repo Application - SAD](https://landmarkcu.sharepoint.com/:w:/r/sites/ITProgrammingDevelopmentTeam/_layouts/15/Doc.aspx?sourcedoc=%7B6BDCF187-B06C-4AB7-A4FA-3138317C3A55%7D&file=LMCU%20Repo%20Manager%20SAD%20V2.docx&action=default&mobileredirect=true)
* This is the software architecture description.
* Site Layout / Reference: Landmark_Site_Reference.XLSX
* This shows urls/locations for source code and deployables and the Azure configuration
* Software Process: landmark_software_process.docx
* Outlines the process for working with the source code.
### Suggestion for IDE
* Visual Studio Code
* Webstorm
### Other Tools:
* [Postman](https://www.postman.com/)
* [React Dev Tools Extension](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
# Architecture
The best explanation for the architecture can be found by referencing the document listed in the resources section above. However if you are looking to do
something specific, here are some tips.
A. Change an environment
1. under the src/environments folder there are currently two environment files to specify settings per environment.
2. The environment is controlled with process.env.REACT_APP_STAGE, see Configuration.ts
# Releases
Releases are handled by anyone on the team with access to Azure DevOps. Generally when a pull request is accepted
to develop, that will be automatically built and deployed to QA. The QA application is here: [https://qa-repomanage.landmarkcu.com]. The
same is true for the back end / API.
To get to UAT, develop must be merged into the release branch and pushed. This will automatically cause the release
branch to be built and the resulting release to be pushed to UAT.
To get to production, release must be merged into master and pushed. This will automatically cause the
code to be built and packaged as artifacts. However the release process is manual, and can be done
here: [https://landmarkcu.visualstudio.com/Repo/_release]
When creating releases, it's important not to forget to make a release and update the change notes as
needed. There is information on creating a release below. An important tip is that on the website
you can hover over the Landmark copyright in the bottom center to see the released client version and build stamp.
## Setup Commitizen and Produce changelogs and release notes
Commitizen is a command line tool that helps format commit messages with a series of prompts that are used to
generate a commit message. We use this tool to standardize our commits for tracking a changelog and producing release
notes.
* `npm install commitizen -g`
* `npm install commitizen --save-dev` // Already done for you, skip.
* `npm install cz-conventional-changelog --save-dev` // Already done for you, skip.
* `npm install husky --save-dev` // Already done for you, skip.
* `npm install --save-dev validate-commit-msg` // Already done for you, skip.
* `npm install -g conventional-changelog-cli`
Instead of committing with `git commit` commit with `git cz`
This will force your messages into the format <type>(<scope>): <subject>
Definition | Description
-----------|------------
feat | A new feature
fix | A bug fix
docs | Documentation only changes
style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
refactor | A code change that neither fixes a bug nor adds a feature
perf | A code change that improves performance
test | Adding missing or correcting existing tests
chore | Changes to the build process or auxiliary tools and libraries such as documentation generation
Here is how semantic-release will map these to release types and how it maps to [semantic verioning](https://semver.org/#summary):
Commit message | Release type
----------------|--------------
`fix(pencil): stop graphite breaking `<br>`when too much pressure applied` | Patch Release (PATCH)
`feat(pencil): add 'graphiteWidth' option` | Feature Release (MINOR)
`BREAKING CHANGE: The graphiteWidth option `<br>`has been removed. The default graphite`<br>` width of 10mm is always used for performance reasons.` | Breaking Release (MAJOR)
## Produce or update the changelog
You shouldn't need to do this, it should be generated for you when you create a release (below)
* Create changelog from scratch: `conventional-changelog -p angular -i CHANGELOG.md -s -r 0`
* Update the changelog: `conventional-changelog -p angular -i CHANGELOG.md -s `
## Produce a set of release notes and bump version
* Use: `npm run release`
* You should check the ChangeLog.MD after this and make any corrections.
Resources:
* http://www.nullfactory.net/2017/04/enforce-commit-message-standards-and-generate-changelogs-via-git/
* https://dev.bleacherreport.com/how-we-use-commitizen-to-clean-up-commit-messages-a16790dcd2fd
* http://devboosts.com/2017/03/28/conventional-commits/
* https://medium.com/@LeonardoBrAu/why-git-squash-and-rebase-commits-and-how-we-do-it-87aa17406e21
# ADA Compliance
This application is targeting level AA ADA Compliance, here is the checklist you should keep in mind when developing:
* Every image, video file, audio file, plug-in, etc. has an alt tag
* Complex graphics are accompanied by detailed text descriptions
* The alt descriptions describe the purpose of the objects
* If an image is also used as a link, the alt tag describes the graphics and the link destination
* Decorative graphics with no other function have empty alt descriptions (alt=””)
* Data tables have the column and row headers appropriately identified (using the tag)
* Make sure the page does not contain repeatedly flashing images
* Check to make sure the page does not contain a strobe effect
* All scripts and plug-ins and the content within them are accessible to assistive technologies, or else an alternative means of accessing equivalent content is provided
* When form controls are text input fields use the LABEL element
* When text is not available use the title attribute
* Include any special instructions within field labels
* Make sure that form fields are in a logical tab order
* Include a ‘Skip Navigation’ button to help those using text readers
* SVG icons should not focusable by tabbing in IE (utilize `focusable="false"` on all icon tags)
## Coding tools for ADA Compliance
* Refer to here for help: https://getbootstrap.com/docs/4.0/getting-started/accessibility/
* Refer to here for practical examples: http://www.punkchip.com/aria-basic-findings/
* use aria-label, aria-expanded, and aria-hidden as needed.
* Bootstrap: sr-only, bootstrap provides sr-only so that you can show things to people with screen readers. (sr-only stands for screen readers only)
# Style Concerns
1) Select boxes title/default should be "Please Choose" if they do not have a default value.
2) Name your actions with the relevant NOUN in them, don't do GET_ALL, do REPOACCOUNT_GET_ALL.
3) Wherever possible reuse an existing component to keep UI consistent across application.
4) It is best to convert literals to constants at the top of the file. If applicable, move this const to the configuration where it can be easily modified later.
* Always keep the web development checklist in mind: http://webdevchecklist.com/
* You should have unit tests for all components and public functions
* Take your time with naming and ask the team for help; but take naming seriously so that your components and services are recognizable.
* Review the .editorconfig file if you are notices style differences in PRs, resolve any problems with the team.
# Libraries
* Redux-router - This router library is used to enable hash routing within the application.
* React-Adal - JWT authentication via Azure AD.
* react-app-polyfill - Add support for IE10/IE11
* react-toastify - Used to notify users of events within the application
# FAQ
Q. What the heck is a 'thunk'?
A. https://daveceddia.com/what-is-a-thunk/
Q. I'm running the unit test and I see output like this:
`HeadlessChrome 68.0.3440 (Windows 10.0.0) XYZComponent should use the service to add address controls to the form group FAILED
[object ErrorEvent] thrown
`
A. Re-run the tests using `npm run test-debug`, which will show the full stack trace but not provide code coverage.
Q. What do I need to know about Security?
A. The major security concerns are outlined in the architecture documents in the share (see resources above). The main security mechanism
is for Landmark employees, and uses the React-ADAL library to authenticate against Azure AD.
Q. Where was this project started from?
A. This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
@@ -0,0 +1,44 @@
# This is the build for Repo Client - A React App.
# All branches are built to test them, but only develop, release, and master are published.
name: 'RepoClient.$(Build.SourceBranchName).$(Date:yyyyMMdd)$(Rev:.r)'
trigger:
branches:
include:
- '*'
pool:
name: LCUOnPrem
demands: npm
steps:
- task: Npm@1
displayName: 'npm install'
inputs:
arguments: '--before=2025-09-1'
verbose: false
- task: Npm@1
displayName: 'npm run test'
inputs:
command: custom
verbose: false
customCommand: 'run-script test-ci'
- task: Npm@1
displayName: 'npm run build $(Build.BuildNumber).$(Build.SourceBranchName)'
inputs:
command: custom
verbose: false
customCommand: 'run build'
- task: PublishBuildArtifacts@1
condition: and (succeeded(), or( eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'master')) )
displayName: 'Publish Artifact: dist_$(Build.SourceBranchName)'
inputs:
PathtoPublish: build
ArtifactName: 'dist'
- task: PublishTestResults@2
displayName: 'Publish Test Results **\test-results.xml'
inputs:
testResultsFiles: '**\test-results.xml'
@@ -0,0 +1,17 @@
module.exports = {
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
{
"name": "repo-db-client",
"version": "1.2.0",
"private": true,
"dependencies": {
"@types/file-saver": "^2.0.1",
"@types/node": "12.0.2",
"@types/react": "16.8.18",
"@types/react-adal": "^0.4.1",
"@types/react-datepicker": "^2.9.3",
"@types/react-dom": "16.8.4",
"@types/react-router-dom": "^4.3.4",
"@types/redux-mock-store": "^1.0.1",
"axios": "^0.18.0",
"azure-maps-control": "^2.3.7",
"bootstrap": "^4.3.1",
"connected-react-router": "^6.5.2",
"file-saver": "^2.0.2",
"font-awesome": "^4.7.0",
"lcu-react-lib": "^2.18.0",
"lodash.clonedeep": "^4.5.0",
"lodash.isequal": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.throttle": "^4.1.1",
"moment": "^2.24.0",
"node": "^13.14.0",
"node-sass": "^4.12.0",
"react": "^16.8.6",
"react-adal": "^0.5.0",
"react-app-polyfill": "^1.0.2",
"react-datepicker": "^2.9.6",
"react-dom": "^16.8.6",
"react-redux": "^7.0.3",
"react-route-guard": "^1.0.7",
"react-router": "^5.3.1",
"react-router-dom": "^5.3.4",
"react-scripts": "^3.4.0",
"react-toastify": "^5.3.2",
"redux": "^4.0.1",
"redux-mock-store": "^1.5.3",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"typescript": "3.4.5",
"xlsx": "^0.15.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test-ci": "react-scripts test --watchAll=false",
"eject": "react-scripts eject",
"initlint": "tslint --init",
"postinstall": "node version.js",
"initcz": "commitizen init cz-conventional-changelog --save-dev --save-exact",
"commitmsg": "validate-commit-msg",
"release": "standard-version",
"test:coverage": "set CI=true && react-scripts test --coverage",
"refreshVSToken": "vsts-npm-auth -config .npmrc"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-transform-arrow-functions": "^7.2.0",
"@types/enzyme": "^3.9.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/faker": "^4.1.8",
"@types/jest": "^24.0.13",
"@types/lodash.isequal": "^4.5.5",
"@types/lodash.pick": "^4.4.6",
"@types/lodash.throttle": "^4.1.6",
"@types/moxios": "^0.4.8",
"@types/react-redux": "^7.0.9",
"@types/redux-thunk": "^2.1.0",
"commitizen": "^3.1.1",
"cz-conventional-changelog": "^2.1.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.14.0",
"git-describe": "^4.0.4",
"husky": "^2.3.0",
"jest": "^24.7.1",
"jest-enzyme": "^7.0.2",
"moxios": "^0.4.0",
"raf": "^3.4.1",
"redux-devtools-extension": "^2.13.8",
"standard-version": "^7.0.0",
"ts-jest": "^24.0.2",
"tslint": "^5.16.0",
"tslint-react": "^4.0.0",
"validate-commit-msg": "^2.14.0",
"vsts-npm-auth": "^0.37.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
},
"validate-commit-msg": {
"types": [
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore",
"revert"
],
"warnOnFail": false,
"maxSubjectLength": 100,
"subjectPattern": "^.*",
"subjectPatternErrorMsg": "Subject must be in format 'Commit message'",
"helpMessage": ""
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/v3.13.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.13.0/mapbox-gl.css' rel='stylesheet' />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Repo Manager</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,45 @@
# Landmark Credit Union
# Developer Environment Setup
# Running this script on a Windows 10 machine should install all of the software you need
# to work on the project for both the client and api.
# You can use 'choco upgrade all' to keep software up to date, but be careful updating Node (see Node section)
Write-Output "Starting environment setup, if you use Visual Studio, or Webstorm, install those separately"
# 1. Create an update function.
function Update-Environment-Path
{
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") `
+ ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
}
# 2. Get the Choco package manager, to enable the rest of this script
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Start-Process powershell.exe "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs; exit }
Invoke-WebRequest https://chocolatey.org/install.ps1 -UseBasicParsing | iex
Update-Environment-Path
# 3. Git for source code management.
choco install git --yes --params '/GitAndUnixToolsOnPath'
Update-Environment-Path
# 4. Node for node and npm.
# 10.14.1 to avoid e2e problems
choco install nodejs --version 10.14.1 --yes
Update-Environment-Path
# 5. VS Code
choco install visualstudiocode --yes # includes dotnet
Update-Environment-Path
code --install-extension CoenraadS.bracket-pair-colorizer
code --install-extension ms-vscode.PowerShell
code --install-extension ms-vscode.csharp
code --install-extension eg2.vscode-npm-script
code --install-extension christian-kohler.npm-intellisense
# 6. major browsers
choco install googlechrome --yes
choco install firefox --yes
Update-Environment-Path
Write-Output "Complete"
@@ -0,0 +1,33 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,18 @@
import React from 'react';
import App from './App';
import { ShallowWrapper, shallow } from 'enzyme';
// - TODO: Removing out test for now - maps js files are causing issues with current version of typescript. Once typescript is updated, add .test back into file name.
describe ('App component', () => {
let wrapper: ShallowWrapper;
beforeEach(() => {
wrapper = shallow(<App />);
});
it ('renders a navigation bar', () => {
const component = wrapper.find(`[data-test='navigationBar']`);
expect(component.length).toBe(1);
});
});
@@ -0,0 +1,41 @@
import { ConnectedRouter } from "connected-react-router";
import React from "react";
import { HashRouter, Route, Switch } from "react-router-dom";
import "./App.css";
import Footer from "./components/footer/Footer";
import AccountPage from "./containers/account-page/AccountPage";
import AdminAuctionHouses from "./containers/admin-auction-houses/AdminAuctionHouses";
import AdminDashBoard from "./containers/admin-dashboard/AdminDashboard";
import AdminRepoCompanies from "./containers/admin-repo-companies/AdminRepoCompanies";
import AdminReporting from "./containers/admin-reporting/AdminReporting";
import FloorCalculation from "./containers/floor-calculation/FloorCalculation";
import NavigationBar from "./containers/navigation-bar/NavigationBar";
import StartPage from "./containers/start-page/StartPage";
import VehicleFloorPricingParameters from "./containers/vehicle-floor-pricing-parameters/VehicleFloorPricingParameters";
import AdminAccountLookup from "./containers/admin-account-lookup/AdminAccountLookup";
import Routes from "./shared/Routes";
import { AppRouteHistory } from "./store/Store";
const App: React.FC = () => {
return (
<ConnectedRouter history={AppRouteHistory}>
<HashRouter>
<NavigationBar data-test="navigationBar"/>
<Switch>
<Route exact={true} path={Routes.Root()} render={(routerProps) => <StartPage {...routerProps} data-test="startPage"/>} />
<Route exact={true} path={Routes.Account_Decl()} render={(routerProps) => <AccountPage {...routerProps} data-test="accountPage"/>} />
<Route exact={true} path={Routes.Admin()} render={(routerProps) => <AdminDashBoard {...routerProps} data-test="adminDashBoardLnk"/>} />
<Route exact={true} path={Routes.Reporting()} render={(routerProps) => <AdminReporting {...routerProps} data-test="adminReportingLnk"/>} />
<Route exact={true} path={Routes.FloorPricing()} render={(routerProps) => <VehicleFloorPricingParameters {...routerProps} data-test="floorPricingParameters"/>} />
<Route exact={true} path={Routes.AdminAuctionHouses()} render={(routerProps) => <AdminAuctionHouses {...routerProps} />} />
<Route exact={true} path={Routes.FloorCalculation_Decl()} render={(routerProps) => <FloorCalculation {...routerProps}/>} />
<Route exact={true} path={Routes.AdminRepoCompanies()} render={(routerProps) => <AdminRepoCompanies {...routerProps} />} />
<Route exact={true} path={Routes.AdminAccountLookup()} render={(routerProps) => <AdminAccountLookup {...routerProps} />} />
</Switch>
<Footer data-test="footer"/>
</HashRouter>
</ConnectedRouter>
);
};
export default App;
@@ -0,0 +1,45 @@
import moxios from 'moxios';
import {applyMiddleware, createStore} from 'redux';
import rootReducer from '../store/Store';
import thunk from 'redux-thunk';
import {getAllAlerts} from './AlertActions';
const testStore = (initialState: any) => {
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
return createStoreWithMiddleware(rootReducer, initialState);
};
describe('getAllAlerts action', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
test('Store updates correctly', () => {
const expectedState = {
"alerts": [
{stuff: 1}
]
};
const store = testStore(expectedState);
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: expectedState
});
});
const beginningState = store.getState().alertState;
store.dispatch(getAllAlerts()).then(() => {
const newState = store.getState().alertState;
expect(newState).toEqual(expectedState);
expect(newState).not.toEqual(beginningState);
});
});
});
@@ -0,0 +1,141 @@
// Import redux types
import { ActionCreator, Dispatch } from 'redux';
import { ThunkAction } from 'redux-thunk';
import axios from 'axios';
// Import Alert Typing
import { IAlert, IAlertState } from '../reducers/AlertReducer';
import Configuration from '../shared/configuration/Configuration';
import ServerLogging from '../shared/logging/ServerLogging';
import UrlFormatting from '../shared/formatting/UrlFormatting';
import moment from 'moment';
import DateConversions from '../shared/conversions/DateConversions';
import {getUser} from '../adalConfig';
const cloneDeep = require('lodash.clonedeep');
// Create Action Constants
export enum AlertActionTypes {
GET_ALL = 'GET_ALL',
GET_ALL_ACTIONS = 'GET_ALL_ACTIONS',
MARK_ALERT_READ = 'MARK_ALERT_READ',
}
// Interface for Get All Action Type
export interface IAlertGetAllAction {
type: AlertActionTypes.GET_ALL;
alerts: IAlert[];
}
export interface IAlertGetAllActionsAction {
type: AlertActionTypes.GET_ALL_ACTIONS;
actions: IAlert[];
}
export interface IAlertMarkReadAction {
type: AlertActionTypes.MARK_ALERT_READ,
alert: IAlert;
}
/*
Combine the action types with a union (we assume there are more)
example: export type AlertActions = IGetAllAction | IGetOneAction ...
*/
export type AlertActions = IAlertGetAllAction | IAlertGetAllActionsAction | IAlertMarkReadAction;
const convertServerDataToAlerts = (alerts: IAlert[]): IAlert[] => {
return alerts.map((alert) => convertServerDataToAlert(alert));
}
const convertServerDataToAlert = (alert: IAlert): IAlert => {
alert.dateMarkedRead = DateConversions.utcToMoment(alert.dateMarkedRead);
alert.dateAdded = DateConversions.utcToMoment(alert.dateAdded)!;
return alert;
};
const createAPIReadyAlert = (alert: IAlert) => {
const clonedAlert = cloneDeep(alert);
clonedAlert.dateAdded = DateConversions.momentToUTC(alert.dateAdded);
clonedAlert.dateMarkedRead = DateConversions.momentToUTC(alert.dateMarkedRead);
return clonedAlert;
}
/* Get All Action
<Promise<Return Type>, State Interface, Type of Param, Type of Action> */
export const getAllAlerts: ActionCreator<
ThunkAction<Promise<any>, IAlertState, null, IAlertGetAllAction>
> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.alerts_unread);
return async (dispatch: Dispatch) => {
try {
const response = await axios.get(url);
const convertedAlerts = convertServerDataToAlerts(response.data);
dispatch({
alerts: convertedAlerts,
type: AlertActionTypes.GET_ALL,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err,{
action: "Get All Alerts",
user: getUser().userName,
url: url
});
}
};
};
// @TODO, the action should just take the type of alert to load?
export const getAllActions: ActionCreator<
ThunkAction<Promise<any>, IAlertState, null, IAlertGetAllActionsAction>
> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.alerts_action_needed);
return async (dispatch: Dispatch) => {
try {
const response = await axios.get(url);
dispatch({
actions: response.data,
type: AlertActionTypes.GET_ALL_ACTIONS,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err,{
action: "Get All Actions",
user: getUser().userName,
url: url
});
}
};
};
export const markAlertRead: ActionCreator<
ThunkAction<Promise<any>, IAlertState, null, IAlertMarkReadAction>
> = (alert: IAlert) => {
const alertPostURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.alerts_put),
{ key: 'alertId', value: alert.id },
);
return async (dispatch: Dispatch) => {
try {
const clonedAlert = cloneDeep(alert) as IAlert;
clonedAlert.dateMarkedRead = moment();
const revertedAlert = createAPIReadyAlert(clonedAlert);
const response = await axios.put(
alertPostURL,
revertedAlert,
);
dispatch({
type: AlertActionTypes.MARK_ALERT_READ,
alert: response.data,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err,{
action: "Mark Alert Read",
user: getUser().userName,
url: alertPostURL,
arguments: alert
});
}
}
};
@@ -0,0 +1,344 @@
import axios from "axios";
import { LocationChangeAction } from "connected-react-router";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IAuctionAssignment, IAuctionAssignmentPostRequest, IAuctionAssignmentState, IAutoIMS, IFloorPriceCalculationResultSet, IFloorPriceValuesUsed } from "../reducers/AuctionAssignmentReducer";
import RepoAccountService from "../services/RepoAccountService";
import Configuration from "../shared/configuration/Configuration";
import DateConversions from "../shared/conversions/DateConversions";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from '../adalConfig'
const cloneDeep = require("lodash.clonedeep");
export enum AuctionAssignmentActionTypes {
GET_FLOOR_PRICE = "AUCTION_ASSIGNMENT_GET_FLOOR_PRICE",
INSERT = "AUCTION_ASSIGNMENT_INSERT",
INSERT_FROM_POST = "AUCTION_ASSIGNMENT_INTSERT_FROM_POST",
INSERT_MULTIPLE = "AUCTION_ASSIGNMENT_INSERT_MULTIPLE",
SET_SELECTED_AUCTION_ASSIGNMENT = "AUCTION_ASSIGNMENT_SET_SELECTED"
}
export interface IAuctionAssignmentGetFloorPrice {
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE;
floorPriceCalculationResultSet: IAsyncStateContainer<IFloorPriceCalculationResultSet>;
}
export interface IAuctionAssignmentInsert {
type: AuctionAssignmentActionTypes.INSERT;
auctionAssignment: IAuctionAssignment;
}
export interface IAuctionAssignmentInsertFromPost {
type: AuctionAssignmentActionTypes.INSERT_FROM_POST;
auctionAssignmentResponse: IAuctionAssignmentPostRequest;
}
export interface IAuctionAssignmentInsertMultiple {
type: AuctionAssignmentActionTypes.INSERT_MULTIPLE;
auctionAssignments: IAuctionAssignment[];
}
export interface IAuctionAssignmentSetSelected {
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT;
auctionAssignmentId: IAsyncStateContainer<number>;
}
export type AuctionAssignmentActions =
IAuctionAssignmentGetFloorPrice |
IAuctionAssignmentInsert |
IAuctionAssignmentInsertMultiple |
IAuctionAssignmentInsertFromPost |
IAuctionAssignmentSetSelected |
LocationChangeAction;
const convertServerDataToAuctionAssignment = (auctionAssignment: IAuctionAssignment): void => {
auctionAssignment.dateSubmitted = DateConversions.utcToMoment(auctionAssignment.dateSubmitted);
auctionAssignment.holdUntil = DateConversions.utcToMoment(auctionAssignment.holdUntil);
auctionAssignment.redeemedDate = DateConversions.utcToMoment(auctionAssignment.redeemedDate);
auctionAssignment.sentTitleDocsDate = DateConversions.utcToMoment(auctionAssignment.sentTitleDocsDate);
if (auctionAssignment.autoIMS) {
const autoIMS: IAutoIMS = auctionAssignment.autoIMS;
autoIMS.assignmentAccepted = DateConversions.utcToMoment(autoIMS.assignmentAccepted);
autoIMS.dateReceivedFromAutoIMS = DateConversions.utcToMoment(autoIMS.dateReceivedFromAutoIMS);
autoIMS.scheduledSaleDate = DateConversions.utcToMoment(autoIMS.scheduledSaleDate);
autoIMS.soldDate = DateConversions.utcToMoment(autoIMS.soldDate);
autoIMS.clientCancelled = DateConversions.utcToMoment(autoIMS.clientCancelled);
autoIMS.inspectedDate = DateConversions.utcToMoment(autoIMS.inspectedDate);
autoIMS.dateofLoss = DateConversions.utcToMoment(autoIMS.dateofLoss);
autoIMS.datePickedUp = DateConversions.utcToMoment(autoIMS.datePickedUp);
autoIMS.dateEnteredAutoIMS = DateConversions.utcToMoment(autoIMS.dateEnteredAutoIMS);
autoIMS.fundsSent = DateConversions.utcToMoment(autoIMS.fundsSent);
autoIMS.titleReceived = DateConversions.utcToMoment(autoIMS.titleReceived);
autoIMS.titleReturned = DateConversions.utcToMoment(autoIMS.titleReturned);
autoIMS.shipmentDate = DateConversions.utcToMoment(autoIMS.shipmentDate);
auctionAssignment.autoIMS = autoIMS;
}
if (auctionAssignment.floorPriceValuesUsed) {
convertServerDataToFloorPriceValuesUsed(auctionAssignment.floorPriceValuesUsed);
}
};
const convertServerDataToFloorPriceValuesUsed = (floorPriceValuesUsed: IFloorPriceValuesUsed): void => {
floorPriceValuesUsed.collateralValuation.lookupDate = DateConversions.utcToMoment(floorPriceValuesUsed.collateralValuation.lookupDate);
};
const createAPIReadyAuctionAssignment = (auctionAssignment: IAuctionAssignment): any => {
const result = cloneDeep(auctionAssignment);
result.dateSubmitted = DateConversions.momentToUTC(auctionAssignment.dateSubmitted);
result.holdUntil = DateConversions.momentToUTC(auctionAssignment.holdUntil);
result.redeemedDate = DateConversions.momentToUTC(auctionAssignment.redeemedDate);
result.titleSentNotificationDate = DateConversions.momentToUTC(auctionAssignment.sentTitleDocsDate);
if (auctionAssignment.autoIMS) {
const autoIMS: any = cloneDeep(auctionAssignment.autoIMS);
autoIMS.assignmentAccepted = DateConversions.momentToUTC(autoIMS.assignmentAccepted);
autoIMS.dateReceivedFromAutoIMS = DateConversions.momentToUTC(autoIMS.dateReceivedFromAutoIMS);
autoIMS.scheduledSaleDate = DateConversions.momentToUTC(autoIMS.scheduledSaleDate);
autoIMS.soldDate = DateConversions.momentToUTC(autoIMS.soldDate);
autoIMS.clientCancelled = DateConversions.momentToUTC(autoIMS.clientCancelled);
autoIMS.inspectedDate = DateConversions.momentToUTC(autoIMS.inspectedDate);
autoIMS.dateofLoss = DateConversions.momentToUTC(autoIMS.dateofLoss);
autoIMS.datePickedUp = DateConversions.momentToUTC(autoIMS.datePickedUp);
autoIMS.dateEnteredAutoIMS = DateConversions.momentToUTC(autoIMS.dateEnteredAutoIMS);
autoIMS.fundsSent = DateConversions.momentToUTC(autoIMS.fundsSent);
autoIMS.titleReceived = DateConversions.momentToUTC(autoIMS.titleReceived);
autoIMS.titleReturned = DateConversions.momentToUTC(autoIMS.titleReturned);
autoIMS.shipmentDate = DateConversions.momentToUTC(autoIMS.shipmentDate);
result.autoIMS = autoIMS;
}
if (auctionAssignment.floorPriceValuesUsed) {
const valuesUsed: any = cloneDeep(auctionAssignment.floorPriceValuesUsed);
const collateralValuation: any = cloneDeep(auctionAssignment.floorPriceValuesUsed.collateralValuation);
collateralValuation.LookUpDate = DateConversions.momentToUTC(collateralValuation.LookUpDate);
valuesUsed.collateralValuation = collateralValuation;
result.valuesUsed = valuesUsed;
}
return result;
};
const createAPIReadyAuctionAssignmentPostRequest = (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest) => {
return {
auctionAssignment: createAPIReadyAuctionAssignment(auctionAssignmentPostRequest.auctionAssignment),
collaterals: auctionAssignmentPostRequest.collaterals,
vehicleLocation: auctionAssignmentPostRequest.vehicleLocation,
primaryCollateralId: auctionAssignmentPostRequest.primaryCollateralId,
};
};
const convertServerDataToAuctionAssignmentPostRequest = (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest): IAuctionAssignmentPostRequest => {
auctionAssignmentPostRequest.collaterals.map((collateral) => RepoAccountService.ConvertResponseToCollateral(collateral));
convertServerDataToAuctionAssignment(auctionAssignmentPostRequest.auctionAssignment);
return {
auctionAssignment: auctionAssignmentPostRequest.auctionAssignment,
collaterals: auctionAssignmentPostRequest.collaterals,
vehicleLocation: auctionAssignmentPostRequest.vehicleLocation,
primaryCollateralId: auctionAssignmentPostRequest.primaryCollateralId,
};
};
export const getAuctionAssignmentsByRepoId: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsertMultiple>> = (repoId: number) => {
const auctionAssignmentURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_by_repo_id),
{ key: "repoId", value: repoId}
);
return async (dispatch: Dispatch<IAuctionAssignmentInsertMultiple>) => {
try {
const response = await axios.get(auctionAssignmentURL);
for (const auctionAssignment of response.data) {
convertServerDataToAuctionAssignment(auctionAssignment);
}
dispatch({
type: AuctionAssignmentActionTypes.INSERT_MULTIPLE,
auctionAssignments: response.data,
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Auction Assignments by Repo ID",
user: getUser().userName,
url: auctionAssignmentURL,
arguments: repoId
});
}
};
};
export const fetchAuctionAssignment: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsert | IAuctionAssignmentSetSelected>> = (id: number) => {
const auctionAssignmentURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_single),
{ key: "auctionAssignmentId", value: id}
);
return async (dispatch: Dispatch<IAuctionAssignmentInsert | IAuctionAssignmentSetSelected>) => {
dispatch({
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT,
auctionAssignmentId: {
state: AsyncState.Loading
}
});
try {
const response = await axios.get(auctionAssignmentURL);
convertServerDataToAuctionAssignment(response.data);
dispatch({
type: AuctionAssignmentActionTypes.INSERT,
auctionAssignment: response.data,
});
dispatch({
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT,
auctionAssignmentId: {
state: AsyncState.Success,
data: id
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Fetch Auction Assignment",
user: getUser().userName,
url: auctionAssignmentURL,
arguments: id
});
dispatch({
type: AuctionAssignmentActionTypes.SET_SELECTED_AUCTION_ASSIGNMENT,
auctionAssignmentId: {
state: AsyncState.Failure
}
});
}
};
};
export const getAuctionAssignmentFloorCalculations: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentGetFloorPrice>> = (auctionAssignmentId: number) => {
const auctionAssignmentURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_floor_price),
{ key: "auctionAssignmentId", value: auctionAssignmentId}
);
return async (dispatch: Dispatch<IAuctionAssignmentGetFloorPrice>) => {
dispatch({
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE,
floorPriceCalculationResultSet: {
state: AsyncState.Loading
}
});
try {
const response = await axios.get(auctionAssignmentURL);
const floorPriceCalculationResultSet = response.data as IFloorPriceCalculationResultSet;
convertServerDataToFloorPriceValuesUsed(floorPriceCalculationResultSet.floorPriceValuesUsed);
dispatch({
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE,
floorPriceCalculationResultSet: {
state: AsyncState.Success,
data: floorPriceCalculationResultSet
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Auction Assignment Floor Calculations",
user: getUser().userName,
url: auctionAssignmentURL,
arguments: auctionAssignmentId
});
dispatch({
type: AuctionAssignmentActionTypes.GET_FLOOR_PRICE,
floorPriceCalculationResultSet: {
state: AsyncState.Failure
}
});
}
};
};
export const postNewAuctionAssignment: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsertFromPost>> = (auctionAssignmentPostRequest: IAuctionAssignmentPostRequest) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment);
return async (dispatch: Dispatch<IAuctionAssignmentInsertFromPost>) => {
try {
const formattedRequest = createAPIReadyAuctionAssignmentPostRequest(auctionAssignmentPostRequest);
const response = await axios.post(url, formattedRequest);
const convertedData = convertServerDataToAuctionAssignmentPostRequest(response.data);
dispatch({
type: AuctionAssignmentActionTypes.INSERT_FROM_POST,
auctionAssignmentResponse: convertedData,
});
toast.success("Auction Assignment Saved.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post New Auction Assignment",
user: getUser().userName,
url: url,
arguments: auctionAssignmentPostRequest
});
}
};
};
export const putAuctionAssignment: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsert>> = (assignment: IAuctionAssignment) => {
const url = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_single),
{ key: "auctionAssignmentId", value: assignment.id}
);
return async (dispatch: Dispatch) => {
try {
const revertedAccount = createAPIReadyAuctionAssignment(assignment);
const response = await axios.put(url, revertedAccount);
const updatedAssignment = response.data;
convertServerDataToAuctionAssignment(updatedAssignment);
dispatch({
type: AuctionAssignmentActionTypes.INSERT,
auctionAssignment: updatedAssignment
});
toast.success("Auction Assignment Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Auction Assignment",
user: getUser().userName,
url: url,
arguments: assignment
});
}
};
};
export const removeAuction: ActionCreator<ThunkAction<Promise<any>, IAuctionAssignmentState, null, IAuctionAssignmentInsert>> = (assignmentId: number) => {
const url = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_assignment_single),
{ key: "auctionAssignmentId", value: assignmentId}
);
return async (dispatch: Dispatch) => {
try {
const response = await axios.delete(url);
const updatedAssignment = response.data;
convertServerDataToAuctionAssignment(updatedAssignment);
dispatch({
type: AuctionAssignmentActionTypes.INSERT,
auctionAssignment: updatedAssignment
});
toast.success("Auction Assignment Removed.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Remove Auction",
user: getUser().userName,
url: url,
arguments: assignmentId
});
}
};
};
@@ -0,0 +1,160 @@
import Axios from "axios";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import {getUser} from "../adalConfig";
import { IAuctionHouse, IAuctionHouseState } from "../reducers/AuctionHouseReducer";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
export enum AuctionHouseActionTypes {
GET_AUCTION_HOUSES = "GET_AUCTION_HOUSES",
POST_AUCTION_HOUSE = "POST_AUCTION_HOUSE",
PUT_AUCTION_HOUSE = "PUT_AUCTION_HOUSE"
}
export interface IAuctionHouseGetAllAction {
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES;
auctionHouses: IAsyncStateContainer<IAuctionHouse[]>;
}
export interface IAuctionHousePostAction {
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE;
auctionHouse: IAsyncStateContainer<IAuctionHouse>;
}
export interface IAuctionHousePutAction {
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE;
auctionHouse: IAsyncStateContainer<IAuctionHouse>;
}
export type AuctionHouseActions = IAuctionHouseGetAllAction | IAuctionHousePostAction | IAuctionHousePutAction;
export const getAllAuctionHouses: ActionCreator<ThunkAction<Promise<any>, IAuctionHouseState, null, IAuctionHouseGetAllAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_house);
return async (dispatch: Dispatch<IAuctionHouseGetAllAction>) =>{
dispatch({
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES,
auctionHouses: {
state: AsyncState.Loading,
},
});
try {
const response = await Axios.get(url);
const auctionHouses = (response.data as IAuctionHouse[]).sort((a, b) =>
a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 0
);
dispatch({
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES,
auctionHouses: {
data: auctionHouses,
state: AsyncState.Success,
},
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Auction Houses",
user: getUser().userName,
url: url,
});
dispatch({
type: AuctionHouseActionTypes.GET_AUCTION_HOUSES,
auctionHouses: {
state: AsyncState.Failure,
},
});
}
};
};
export const postAuctionHouse: ActionCreator<ThunkAction<Promise<any>, IAuctionHouseState, null, IAuctionHousePostAction>> = (newAuctionHouse: IAuctionHouse) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_house);
return async (dispatch: Dispatch<IAuctionHousePostAction>) =>{
try {
dispatch({
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Loading,
}
});
const response = await Axios.post(url, newAuctionHouse);
dispatch({
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Success,
data: response.data
}
});
toast.success("Auction House Added.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post New Auction House",
user: getUser().userName,
url: url,
arguments: newAuctionHouse
});
dispatch({
type: AuctionHouseActionTypes.POST_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Failure,
}
});
}
};
};
export const putAuctionHouse: ActionCreator<ThunkAction<Promise<any>, IAuctionHouseState, null, IAuctionHousePutAction>> = (updatedAuctionHouse: IAuctionHouse) => {
const url = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.auction_house_by_auction_id),
{key:"auctionId", value: updatedAuctionHouse.auctionID},
);
return async (dispatch: Dispatch<IAuctionHousePutAction>) =>{
try {
dispatch({
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Loading,
}
});
const response = await Axios.put(url, updatedAuctionHouse);
dispatch({
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Success,
data: response.data
}
});
toast.success("Auction House Added.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Updated Auction House",
user: getUser().userName,
url: url,
arguments: updatedAuctionHouse
});
dispatch({
type: AuctionHouseActionTypes.PUT_AUCTION_HOUSE,
auctionHouse: {
state: AsyncState.Failure,
}
});
}
};
};
@@ -0,0 +1,191 @@
import Axios from "axios";
import {ActionCreator, Dispatch} from "redux";
import {ThunkAction} from "redux-thunk";
import {getUser} from "../adalConfig";
import {ICollectionsAccount, ICollectionsAccountState} from "../reducers/CollectionsAccountReducer";
import CollectionsAccountService from "../services/CollectionsAccountService";
import {IRepoAccount} from "../services/service-types/RepoAccountTypes";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {IAppState} from "../store/Store";
export enum CollectionsAccountActionTypes {
GET_COLLECTIONS_ACCOUNT = "GET_COLLECTIONS_ACCOUNT",
SET_SEARCH_PHRASE = "SET_SEARCH_PHRASE",
FIND_ASSOCIATED_REPOS = "FIND_ASSOCIATED_REPOS",
PUT_ACCOUNT_ON_QUEUE = "PUT_ACCOUNT_ON_QUEUE"
}
export interface ICollectionsAccountGetAction {
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT;
collectionsAccount: IAsyncStateContainer<ICollectionsAccount[]>;
}
export interface ICollectionsSetSearchPhraseAction {
type: CollectionsAccountActionTypes.SET_SEARCH_PHRASE;
searchPhrase: string;
}
export interface ICollectionsfindAssociatedRepoAccountAction {
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS;
associatedRepoAccounts: IAsyncStateContainer<IRepoAccount[]>;
}
export interface ICollectionsAccountPutAction {
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE;
didAddToQueue: IAsyncStateContainer<boolean>;
}
export type CollectionsAccountActions =
ICollectionsAccountGetAction
| ICollectionsSetSearchPhraseAction
| ICollectionsfindAssociatedRepoAccountAction
| ICollectionsAccountPutAction;
export const getCollectionsAccount: ActionCreator<ThunkAction<Promise<any>, ICollectionsAccountState, null, ICollectionsAccountGetAction>> = (accountNo: string) => {
const url = UrlFormatting.ReplaceURLParams(Configuration.PrefixCollectionsUrl(Configuration.Get.urls.collections.collections_account), {
key: "accountNo",
value: Configuration.Get.collectionsAccountNoPrefix + accountNo
});
return async (dispatch: Dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT,
collectionsAccount: {
state: AsyncState.Loading
}
});
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
state: AsyncState.Inactive,
data: false
}
});
try {
const response = await Axios.get(url);
dispatch({
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT,
collectionsAccount: {
data: CollectionsAccountService.mapResponseToCollectionsAccount(response.data),
state: AsyncState.Success
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Collections Account",
user: getUser().userName,
url: url,
arguments: [accountNo]
});
dispatch({
type: CollectionsAccountActionTypes.GET_COLLECTIONS_ACCOUNT,
collectionsAccount: {
state: AsyncState.Failure
}
});
}
};
};
export const setSearchPhraseCollectionsAccounts: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, ICollectionsSetSearchPhraseAction | ICollectionsAccountGetAction>> = (searchPhrase: string) => {
return async (dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: searchPhrase
});
};
};
export const findAssociatedRepoAccount: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, ICollectionsfindAssociatedRepoAccountAction>> = (collectionsAccount: ICollectionsAccount) => {
const url = UrlFormatting.ReplaceURLParams(Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single_by_account_no), {
key: "accountNo",
value: collectionsAccount.accountIdentifier
});
return async (dispatch: Dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
state: AsyncState.Loading
}
});
try {
const response = await Axios.get(url);
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
data: response.data,
state: AsyncState.Success
}
});
} catch (err) {
if( err && err.response && err.response.status !== 404 ) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Find Associated Repos",
user: getUser().userName,
url: url,
arguments: [collectionsAccount.accountIdentifier]
});
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
state: AsyncState.Failure
}
});
} else {
dispatch({
type: CollectionsAccountActionTypes.FIND_ASSOCIATED_REPOS,
associatedRepoAccounts: {
data: [],
state: AsyncState.Success
}
});
}
}
};
};
export const putAccountOnRepoQueue: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, ICollectionsAccountPutAction>> = (collectionsAccount: ICollectionsAccount) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.put_to_repo_queue);
const queueRequest = {
accountNo: collectionsAccount.accountIdentifier
};
return async (dispatch: Dispatch) => {
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
state: AsyncState.Loading,
data: true
}
});
try {
await Axios.post(url, queueRequest);
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
state: AsyncState.Success,
data: true
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Account On Repo Queue",
user: getUser().userName,
url: url,
arguments: [collectionsAccount]
});
dispatch({
type: CollectionsAccountActionTypes.PUT_ACCOUNT_ON_QUEUE,
didAddToQueue: {
AsyncState: AsyncState.Failure,
data: true
}
});
}
};
};
@@ -0,0 +1,78 @@
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IFactoidState } from "../reducers/FactoidReducer";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import Axios from "axios";
import Configuration from "../shared/configuration/Configuration";
import ServerLogging from "../shared/logging/ServerLogging";
import { IFactoid, IFactoidResponse } from "../services/service-types/FactoidTypes";
import { RepoAccountActionTypes, IRepoAccountInsertAccounts } from "./RepoAccountActions";
import FactoidService from "../services/FactoidService";
import {getUser} from "../adalConfig";
export enum FactoidActionTypes {
SET_SELECTED_FACTOID_INDEX = 'FACTOID_SET_INDEX',
GET_ALL_FACTOIDS = 'FACTOIDs_GET_ALL'
}
export interface IFactoidSetSelectedIndexAction {
type: FactoidActionTypes.SET_SELECTED_FACTOID_INDEX;
index: number;
}
export interface IFactoidGetAllActions {
type: FactoidActionTypes.GET_ALL_FACTOIDS;
factoids: IAsyncStateContainer<IFactoid[]>;
}
export type FactoidActions =
IFactoidSetSelectedIndexAction &
IFactoidGetAllActions;
export const setSelectedFactoidIndex = (index: number): IFactoidSetSelectedIndexAction => {
return {
type: FactoidActionTypes.SET_SELECTED_FACTOID_INDEX,
index: index
}
};
export const getAllFactoids: ActionCreator<ThunkAction<Promise<any>, IFactoidState, null, IFactoidGetAllActions>> = (factoidState: IFactoidState) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.factoids_get_all);
return async(dispatch: Dispatch<IFactoidGetAllActions | IRepoAccountInsertAccounts>) => {
dispatch({
type: FactoidActionTypes.GET_ALL_FACTOIDS,
factoids: {
state: AsyncState.Loading
}
});
try {
const response = await Axios.get(url);
const factoidResponses = response.data as IFactoidResponse[];
dispatch({
type: RepoAccountActionTypes.INSERT_ACCOUNTS,
repoAccounts: FactoidService.ExtractRepoAccountsFromFactoidResponses(factoidResponses)
});
dispatch({
type: FactoidActionTypes.GET_ALL_FACTOIDS,
factoids: {
state: AsyncState.Success,
data: FactoidService.CreateFactoidsFromResponse(factoidResponses)
}
});
} catch(err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Factoids",
user: getUser().userName,
url: url,
arguments: factoidState
});
dispatch({
type: FactoidActionTypes.GET_ALL_FACTOIDS,
factoids: {
state: AsyncState.Failure
}
});
}
};
};
@@ -0,0 +1,45 @@
import moxios from 'moxios';
import {applyMiddleware, createStore} from 'redux';
import rootReducer from '../store/Store';
import thunk from 'redux-thunk';
import {markCollateralRetitled} from './RepoAccountActions'
const testStore = (initialState: any) => {
const createStoreWithMiddleware = applyMiddleware(thunk)(createStore);
return createStoreWithMiddleware(rootReducer, initialState);
}
describe('markCollateralRetitled action', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
test('Store updates correctly', () => {
const expectedState = {
"repoAccounts": [
{stuff: 1}
]
};
const store = testStore(expectedState);
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: expectedState
});
});
const beginningState = store.getState().repoAccountState;
store.dispatch(markCollateralRetitled()).then(() => {
const newState = store.getState().repoAccountState;
expect(newState).toEqual(expectedState);
expect(newState).not.toEqual(beginningState);
});
});
});
@@ -0,0 +1,546 @@
import axios from "axios";
import { LocationChangeAction } from "connected-react-router";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import RepoAccountService from "../services/RepoAccountService";
import { ICollateral, IRefundStatus, IRepoAccount, IRepoAccountFilters, IRepoCompanySelection, IRepoCompanySelectionPostResponse } from "../services/service-types/RepoAccountTypes";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IPagedResponse } from "../shared/types/IPagedResponse";
import { IAppState } from "../store/Store";
import { IAuctionAssignmentInsertFromPost } from "./AuctionAssignmentActions";
import {getUser} from "../adalConfig";
// Create Action Constants
export enum RepoAccountActionTypes {
SET_SELECTED_ACCOUNT = "REPOACCOUNT_SET_SELECTED_ACCOUNT",
INSERT_ONE_ACCOUNT = "REPOACCOUNT_INSERT_ONE",
INSERT_ACCOUNTS = "REPOACCOUNT_INSERT_ACCOUNTS",
SET_FILTERED_ACCOUNTS = "REPOACCOUNT_SET_FILTERED_ACCOUNTS",
INSERT_COLLATERAL = "REPOACCOUNT_INSERT_COLLATERAL",
SET_FILTERS = "REPOACCOUNT_SET_FILTERS",
UPDATE_REFUND_STATUS = "REPOACCOUNT_UPDATE_REFUND_STATUS",
SET_SEARCH_PHRASE = "REPOACCOUNT_SET_SEARCH_PHRASE"
}
export interface IRepoAccountSelectAccountAction {
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT;
repoId: IAsyncStateContainer<number>;
}
export interface IRepoAccountSetFilteredAccountsAction {
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS;
filteredAccounts: IAsyncStateContainer<IPagedResponse<number>>;
}
export interface IRepoAccountInsertOneAccount {
type: RepoAccountActionTypes.INSERT_ONE_ACCOUNT;
repoAccount: IRepoAccount;
}
export interface IRepoAccountInsertAccounts {
type: RepoAccountActionTypes.INSERT_ACCOUNTS;
repoAccounts: IRepoAccount[];
}
export interface IRepoAccountInsertCollateral {
type: RepoAccountActionTypes.INSERT_COLLATERAL;
collateral: ICollateral;
}
export interface IRepoAccountSetAccountFiltersAction {
type: RepoAccountActionTypes.SET_FILTERS;
filters: IRepoAccountFilters;
}
export interface IRepoAccountUpdateRefundStatus {
type: RepoAccountActionTypes.UPDATE_REFUND_STATUS;
refundStatus: IRefundStatus;
}
export interface IRepoAccountSetSearchPhraseAction {
type: RepoAccountActionTypes.SET_SEARCH_PHRASE;
searchPhrase: string;
}
/*
Combine the action types with a union (we assume there are more)
example: export type RepoAccountActions = IGetAllAction | IGetOneAction ...
*/
export type RepoAccountActions =
IRepoAccountSelectAccountAction |
IRepoAccountSetFilteredAccountsAction |
IAuctionAssignmentInsertFromPost |
IRepoAccountInsertOneAccount |
IRepoAccountInsertAccounts |
IRepoAccountInsertCollateral |
IRepoAccountSetAccountFiltersAction |
IRepoAccountUpdateRefundStatus |
IRepoAccountSetSearchPhraseAction |
LocationChangeAction;
export const getInitialRepoAccounts: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertAccounts | IRepoAccountSetFilteredAccountsAction | IRepoAccountSetFilteredAccountsAction>> = () => {
return async (dispatch, getState) => {
// Clear filters, as this fetch will not match the current filters
const newFilter: IRepoAccountFilters = {onlyOpenAccounts: getState().repoAccountState.accountFilters.onlyOpenAccounts};
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: getState().repoAccountState.filteredAccounts.state
}
});
await postFilteredAccountsFromURL(Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_filter), newFilter, dispatch);
};
};
export const setSearchPhrase = (searchPhrase: string): IRepoAccountSetSearchPhraseAction => {
return {
type: RepoAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: searchPhrase
};
};
export const goToNextRepoAccountPage: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>> = () => {
return async (dispatch, getState) => {
const currentFilters = getState().repoAccountState.accountFilters;
const filterPageResponse = getState().repoAccountState.filteredAccounts.data;
if (filterPageResponse && filterPageResponse.nextPage) {
await postFilteredAccountsFromURL(filterPageResponse.nextPage, currentFilters, dispatch);
}
};
};
export const goToPreviousRepoAccountPage: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>> = () => {
return async (dispatch, getState) => {
const currentFilters = getState().repoAccountState.accountFilters;
const filterPageResponse = getState().repoAccountState.filteredAccounts.data;
if (filterPageResponse && filterPageResponse.previousPage) {
await postFilteredAccountsFromURL(filterPageResponse.previousPage, currentFilters, dispatch);
}
};
};
export const fetchSelectedRepoAccount: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertOneAccount | IRepoAccountSelectAccountAction>> = (repoId: number) => {
const singleRepoURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_single),
{key: "repoId", value: repoId},
);
return async (dispatch) => {
// Since we are fetching the selected account,
// we will need to dispatch both to insert the retrieved
// account, as well as to update the selected account
dispatch({
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT,
repoId: {
state: AsyncState.Loading
}
});
try {
const response = await axios.get(singleRepoURL);
RepoAccountService.ConvertResponseToRepoAccount(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_ONE_ACCOUNT,
repoAccount: response.data
});
dispatch({
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT,
repoId: {
state: AsyncState.Success,
data: response.data.repoId
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Fetch Selected Repo Account",
user: getUser().userName,
url: singleRepoURL,
arguments: repoId
});
dispatch({
type: RepoAccountActionTypes.SET_SELECTED_ACCOUNT,
repoId: {
state: AsyncState.Failure
}
});
}
};
};
export const searchRepoAccounts: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetFilteredAccountsAction | IRepoAccountSetAccountFiltersAction | IRepoAccountSetSearchPhraseAction | IRepoAccountInsertAccounts>> = (searchPhrase: string) => {
const parameterizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_search),
{ key: "searchPhrase", value: searchPhrase }
);
// Since we are fetching the currently filtered accounts,
// we will need to dispatch both to insert the retrieved
// account, as well as to update the currently filtered accounts
return async (dispatch) => {
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Loading
}
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERS,
filters: {}
});
dispatch({
type: RepoAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: searchPhrase
});
try {
const response = await axios.get(parameterizedUrl);
RepoAccountService.ConvertResponseToRepoAccounts(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_ACCOUNTS,
repoAccounts: response.data
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Success,
data: {
items: RepoAccountService.CreateRepoAccountIdMap(response.data),
totalItems: response.data.length,
currentPage: 1,
totalPages: 1
}
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Search Repo Accoutns",
user: getUser().userName,
url: parameterizedUrl,
arguments: searchPhrase
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Failure
}
});
}
};
};
export const removeRepoAccountFromAlertFilter: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetFilteredAccountsAction>> = (alertMessage: string, repoId: number) => {
return async (dispatch, getState) => {
const filteredAccounts = getState().repoAccountState.filteredAccounts;
const accountFilters = getState().repoAccountState.accountFilters;
if (filteredAccounts.data && alertMessage === accountFilters.alertMessage) {
const indexOfEl = filteredAccounts.data.items.indexOf(repoId);
if (indexOfEl >= 0) {
const updatedAccountItems = filteredAccounts.data.items.slice();
updatedAccountItems.splice(indexOfEl, 1);
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
data: {
...filteredAccounts.data,
totalItems: filteredAccounts.data.totalItems - 1,
items: updatedAccountItems
},
state: filteredAccounts.state
}
});
}
}
};
};
export const fetchRepoAccountsViaFilters: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountSetSearchPhraseAction | IRepoAccountInsertAccounts>> =
(filters: IRepoAccountFilters) => {
return async (dispatch) => {
dispatch({
type: RepoAccountActionTypes.SET_FILTERS,
filters: filters
});
dispatch({
type: RepoAccountActionTypes.SET_SEARCH_PHRASE,
searchPhrase: ""
});
await postFilteredAccountsFromURL(Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts_filter), filters, dispatch);
};
};
export const postToNextFilterPage: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>> =
(nextPageUrl: string, filters: IRepoAccountFilters) => {
return async (dispatch: Dispatch<IRepoAccountSetAccountFiltersAction | IRepoAccountSetFilteredAccountsAction | IRepoAccountInsertAccounts>) => {
await postFilteredAccountsFromURL(nextPageUrl, filters, dispatch);
};
};
export const putRepoAccount: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertOneAccount>> = (account: IRepoAccount) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.accounts) + "/" + account.repoId;
return async (dispatch: Dispatch<IRepoAccountInsertOneAccount>) => {
try {
const response = await axios.put(url, account);
RepoAccountService.ConvertResponseToRepoAccount(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_ONE_ACCOUNT,
repoAccount: response.data
});
toast.success("Account Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Repo Account",
user: getUser().userName,
url: url,
arguments: account
});
}
};
};
export const putCollateral: ActionCreator<ThunkAction<Promise<any>, ICollateral, null, IRepoAccountInsertCollateral>> = (collateral: ICollateral) => {
const singleCollateralUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.collateral_single),
{key: "collateralId", value: collateral.collateralId},
);
return async (dispatch) => {
try {
const response = await axios.put(
singleCollateralUrl,
collateral
);
RepoAccountService.ConvertResponseToCollateral(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: response.data
});
toast.success("Collateral Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Collateral",
user: getUser().userName,
url: singleCollateralUrl,
arguments: collateral
});
}
};
};
export const putRepoCompanySelection: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertCollateral>> = (repoCompanySelection: IRepoCompanySelection, collateral: ICollateral) => {
const repoCompanySelectionPutURL = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_company_selection_single),
{key: "repoCompanySelectionId", value: repoCompanySelection.id},
);
return async (dispatch) => {
try {
const response = await axios.put(
repoCompanySelectionPutURL,
repoCompanySelection
);
RepoAccountService.ConvertResponseToRepoCompanySelection(response.data);
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: RepoAccountService.CreateCollateralFromRepoCompanySelection(collateral, repoCompanySelection)
});
toast.success("Repo Selection Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Repo Company Selection",
user: getUser().userName,
url: repoCompanySelectionPutURL,
arguments: [repoCompanySelection, collateral]
});
}
};
};
export const postRepoCompanySelection: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertCollateral>> = (repoCompanySelection: IRepoCompanySelection, collateral: ICollateral) => {
const repoCompanySelectionPostURL = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_company_selection);
return async (dispatch) => {
try {
const response = await axios.post(
repoCompanySelectionPostURL,
repoCompanySelection
);
let responseData = response.data as IRepoCompanySelectionPostResponse;
const repoCompanySelectionArray = [
responseData.newRepoCompanySelection
];
RepoAccountService.ConvertResponseToRepoCompanySelection(responseData.newRepoCompanySelection);
if (responseData.previousRepoCompanySelection) {
RepoAccountService.ConvertResponseToRepoCompanySelection(responseData.previousRepoCompanySelection);
repoCompanySelectionArray.push(responseData.previousRepoCompanySelection);
}
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: RepoAccountService.CreateCollateralFromRepoCompanySelection(collateral, repoCompanySelectionArray)
});
toast.success("Repo Selection Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Repo Company Selection",
user: getUser().userName,
url: repoCompanySelectionPostURL,
arguments: [repoCompanySelection, collateral]
});
}
};
};
export const putRefundStatus: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountUpdateRefundStatus>> = (refundStatus: IRefundStatus) => {
const refundStatusPutUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.refund_status_single),
{key: "repoId", value: refundStatus.repoId}
);
return async (dispatch) => {
try {
const response = await axios.put(refundStatusPutUrl, refundStatus);
RepoAccountService.ConvertResponseToRefundStatus(response.data);
dispatch({
type: RepoAccountActionTypes.UPDATE_REFUND_STATUS,
refundStatus: response.data
});
toast.success("Refund Status Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Refund Status",
user: getUser().userName,
url: refundStatusPutUrl,
arguments: refundStatus
});
}
};
};
export const postRefundStatus: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountUpdateRefundStatus>> = (refundStatus: IRefundStatus) => {
const refundStatusPutUrl = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.refund_status);
return async (dispatch) => {
try {
const response = await axios.post(refundStatusPutUrl, refundStatus);
RepoAccountService.ConvertResponseToRefundStatus(response.data);
dispatch({
type: RepoAccountActionTypes.UPDATE_REFUND_STATUS,
refundStatus: response.data
});
toast.success("Refund Status Created.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Refund Status",
user: getUser().userName,
url: refundStatusPutUrl,
arguments: refundStatus
});
}
};
};
export const refreshCollateral: ActionCreator<ThunkAction<Promise<any>, IAppState, {}, IRepoAccountInsertCollateral>> = (
collateral: ICollateral, memberNo: number, loanNo: number) => {
const coresCollateralUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixCoreUrl(Configuration.Get.urls.core.loan_collateral),
{key: "loanNo", value: loanNo},
);
return async (dispatch) => {
try {
const singleRepoCollateralUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.collateral_single),
{key: "collateralId", value: collateral.collateralId},
);
const response = await axios.get(coresCollateralUrl);
const updatedCollateral = updateCollateralWithCoreDetails(response.data, collateral);
await axios.put(
singleRepoCollateralUrl,
updatedCollateral,
);
dispatch({
type: RepoAccountActionTypes.INSERT_COLLATERAL,
collateral: updatedCollateral,
});
toast.success("Collateral Refreshed.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Refresh Collateral",
user: getUser().userName,
url: coresCollateralUrl,
arguments: [collateral, memberNo, loanNo]
});
}
};
};
const updateCollateralWithCoreDetails = (data: any[], collateral: ICollateral): ICollateral => {
// eslint-disable-next-line
const found = data.find(c => c.collateralId == collateral.refCollateralId);
if (found.other.miscellaneousInformation != null) {
collateral.vin = found.other.miscellaneousInformation;
}
if (found.vehicle != null) {
collateral.make = found.vehicle.make;
collateral.model = found.vehicle.model;
collateral.vin = found.vehicle.vin;
collateral.year = found.vehicle.year;
}
return collateral;
};
const fetchFilteredAccountsFromAsyncCallback = async (
repoAccountFetch: () => Promise<IPagedResponse<IRepoAccount>>,
dispatch: Dispatch<IRepoAccountInsertAccounts | IRepoAccountSetFilteredAccountsAction>
) => {
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Loading
}
});
try {
const pagedResponse = await repoAccountFetch();
RepoAccountService.ConvertResponseToRepoAccounts(pagedResponse.items);
dispatch({
type: RepoAccountActionTypes.INSERT_ACCOUNTS,
repoAccounts: pagedResponse.items
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Success,
data: {
items: RepoAccountService.CreateRepoAccountIdMap(pagedResponse.items),
totalItems: pagedResponse.totalItems,
nextPage: pagedResponse.nextPage,
previousPage: pagedResponse.previousPage,
currentPage: pagedResponse.currentPage,
totalPages: pagedResponse.totalPages
}
}
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Fetch Filtered Accounts With Async",
user: getUser().userName,
});
dispatch({
type: RepoAccountActionTypes.SET_FILTERED_ACCOUNTS,
filteredAccounts: {
state: AsyncState.Failure
}
});
}
};
const postFilteredAccountsFromURL = async (url: string, filters: IRepoAccountFilters, dispatch: Dispatch<IRepoAccountInsertAccounts | IRepoAccountSetFilteredAccountsAction>) => {
fetchFilteredAccountsFromAsyncCallback(async () => {
const response = await axios.post(url, filters);
return response.data as IPagedResponse<IRepoAccount>;
}, dispatch);
};
@@ -0,0 +1,110 @@
import axios from "axios";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import {ThunkAction} from "redux-thunk";
import { IRepoCompany, IRepoCompanyState } from "../reducers/RepoCompanyReducer";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from "../adalConfig";
export enum RepoCompanyActionTypes {
SET_REPO_COMPANIES = "GET_REPO_COMPANIES",
INSERT_SINGLE_REPO_COMPANY = "INSERT_SINGLE_REPO_COMPANY"
}
export interface IRepoCompanyInsertAction {
type: RepoCompanyActionTypes.SET_REPO_COMPANIES;
repoCompanies: IAsyncStateContainer<IRepoCompany[]>;
}
export interface IRepoCompanyInsertSingleAction {
type: RepoCompanyActionTypes.INSERT_SINGLE_REPO_COMPANY;
repoCompany: IRepoCompany;
}
export type RepoCompanyActions = IRepoCompanyInsertAction | IRepoCompanyInsertSingleAction;
export const getAllRepoCompanies: ActionCreator<ThunkAction<Promise<any>, IRepoCompanyState, null, IRepoCompanyInsertAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_companies);
return async (dispatch: Dispatch<IRepoCompanyInsertAction>) =>{
dispatch({
type: RepoCompanyActionTypes.SET_REPO_COMPANIES,
repoCompanies: {
state: AsyncState.Loading,
},
});
try {
const response = await axios.get(url);
dispatch({
type: RepoCompanyActionTypes.SET_REPO_COMPANIES,
repoCompanies: {
data: response.data,
state: AsyncState.Success,
},
});
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Repo Companies",
user: getUser().userName,
url: url
});
dispatch({
type: RepoCompanyActionTypes.SET_REPO_COMPANIES,
repoCompanies: {
state: AsyncState.Failure,
},
});
}
};
};
export const putRepoCompany: ActionCreator<ThunkAction<Promise<any>, IRepoCompanyState, null, IRepoCompanyInsertSingleAction>> = (repoCompany: IRepoCompany) => {
const parametrizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_companies_single),
{ key: "repoCompanyId", value: repoCompany.id }
);
return async (dispatch: Dispatch<IRepoCompanyInsertSingleAction>) =>{
try {
const response = await axios.put(parametrizedUrl, repoCompany);
const repoCompanyFromServer = response.data as IRepoCompany;
dispatch({
type: RepoCompanyActionTypes.INSERT_SINGLE_REPO_COMPANY,
repoCompany: repoCompanyFromServer
});
toast.success("Repo Company Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Repo Company",
user: getUser().userName,
url: parametrizedUrl,
arguments: repoCompany
});
}
};
};
export const postRepoCompany: ActionCreator<ThunkAction<Promise<any>, IRepoCompanyState, null, IRepoCompanyInsertSingleAction>> = (repoCompany: IRepoCompany) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.repo_companies);
return async (dispatch: Dispatch<IRepoCompanyInsertSingleAction>) =>{
try {
const response = await axios.post(url, repoCompany);
const repoCompanyFromServer = response.data as IRepoCompany;
dispatch({
type: RepoCompanyActionTypes.INSERT_SINGLE_REPO_COMPANY,
repoCompany: repoCompanyFromServer
});
toast.success("Repo Company Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Repo Company",
user: getUser().userName,
url: url,
arguments: repoCompany
});
}
};
};
@@ -0,0 +1,44 @@
import Axios from "axios";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IAdminReportForm, IReport, IReportingState } from "../reducers/ReportingReducer";
import AsyncState from "../shared/enumerations/AsyncState";
import ServerLogging from "../shared/logging/ServerLogging";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from "../adalConfig";
export enum ReportingActionTypes {
SET_REPORT = "SET_REPORT_DATA"
}
export interface IReportingGetReportAction {
type: ReportingActionTypes.SET_REPORT;
reportForm: IAdminReportForm;
currentReport: IAsyncStateContainer<IReport>;
}
export type ReportingActions = IReportingGetReportAction;
export const selectReport: ActionCreator<ThunkAction<Promise<any>, IReportingState, null, IReportingGetReportAction>> = (reportForm: IAdminReportForm) => {
return async (dispatch: Dispatch<IReportingGetReportAction>) => {
if (!reportForm.reportUrl) {
dispatch({
type: ReportingActionTypes.SET_REPORT,
reportForm: reportForm,
currentReport: {
state: AsyncState.Inactive,
},
});
return;
}
dispatch({
type: ReportingActionTypes.SET_REPORT,
reportForm: reportForm,
currentReport: {
state: AsyncState.Loading,
},
});
return;
};
};
@@ -0,0 +1,111 @@
import axios from "axios";
import { toast } from "react-toastify";
import { ActionCreator, Dispatch } from "redux";
import {ThunkAction} from "redux-thunk";
import { ISavedLocation, ISavedLocationState } from "../reducers/SavedLocationReducer";
import Configuration from "../shared/configuration/Configuration";
import AsyncState from "../shared/enumerations/AsyncState";
import UrlFormatting from "../shared/formatting/UrlFormatting";
import ServerLogging from "../shared/logging/ServerLogging";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {getUser} from "../adalConfig";
export enum SavedLocationActionTypes {
SET_SAVED_LOCATIONS = "GET_SAVED_LOCATIONS",
INSERT_SINGLE_SAVED_LOCATION = "INSERT_SINGLE_SAVED_LOCATION"
}
export interface ISavedLocationInsertAction {
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS;
savedLocations: IAsyncStateContainer<ISavedLocation[]>;
}
export interface ISavedLocationInsertSingleAction {
type: SavedLocationActionTypes.INSERT_SINGLE_SAVED_LOCATION;
savedLocation: ISavedLocation;
}
export type SavedLocationActions = ISavedLocationInsertAction | ISavedLocationInsertSingleAction;
export const getAllSavedLocations: ActionCreator<ThunkAction<Promise<any>, ISavedLocationState, null, ISavedLocationInsertAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.saved_locations);
return async (dispatch: Dispatch<ISavedLocationInsertAction>) =>{
dispatch({
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS,
savedLocations: {
state: AsyncState.Loading,
},
});
try {
const response = await axios.get(url);
dispatch({
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS,
savedLocations: {
data: response.data,
state: AsyncState.Success,
},
});
} catch (err) {
dispatch({
type: SavedLocationActionTypes.SET_SAVED_LOCATIONS,
savedLocations: {
state: AsyncState.Failure,
},
});
ServerLogging.ResolveErrorWithToast(err, {
action: "Get All Saved Locations",
user: getUser().userName,
url: url,
});
}
};
};
export const putSavedLocation: ActionCreator<ThunkAction<Promise<any>, ISavedLocationState, null, ISavedLocationInsertSingleAction>> = (savedLocation: ISavedLocation) => {
const parametrizedUrl = UrlFormatting.ReplaceURLParams(
Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.saved_locations_single),
{ key: "savedLocationId", value: savedLocation.id }
);
return async (dispatch: Dispatch<ISavedLocationInsertSingleAction>) =>{
try {
const response = await axios.put(parametrizedUrl, savedLocation);
const savedLocationFromServer = response.data as ISavedLocation;
dispatch({
type: SavedLocationActionTypes.INSERT_SINGLE_SAVED_LOCATION,
savedLocation: savedLocationFromServer
});
toast.success("Saved Location Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Saved Location",
user: getUser().userName,
url: parametrizedUrl,
arguments: savedLocation
});
}
};
};
export const postSavedLocation: ActionCreator<ThunkAction<Promise<any>, ISavedLocationState, null, ISavedLocationInsertSingleAction>> = (savedLocation: ISavedLocation) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.saved_locations);
return async (dispatch: Dispatch<ISavedLocationInsertSingleAction>) =>{
try {
const response = await axios.post(url, savedLocation);
const savedLocationFromServer = response.data as ISavedLocation;
dispatch({
type: SavedLocationActionTypes.INSERT_SINGLE_SAVED_LOCATION,
savedLocation: savedLocationFromServer
});
toast.success("Saved Location Updated.", ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Post Saved Location",
user: getUser().userName,
url: url,
arguments: savedLocation
});
}
};
};
@@ -0,0 +1,19 @@
import {UserInfo} from "react-adal";
export enum SecurityActionTypes {
APPLY_USER = 'APPLY_USER',
APPLY_TOKEN = 'APPLY_TOKEN',
}
export interface ISecurityApplyAction {
type: SecurityActionTypes.APPLY_USER;
user: UserInfo;
}
export interface ISecurityTokenAction {
type: SecurityActionTypes.APPLY_TOKEN;
userToken: string;
}
export type SecurityActions = ISecurityApplyAction | ISecurityTokenAction;
@@ -0,0 +1,43 @@
import { ISettingsState } from "../reducers/SettingsReducer";
import { ActionCreator, Dispatch } from "redux";
import {ThunkAction} from 'redux-thunk'
import axios from 'axios';
import Configuration from "../shared/configuration/Configuration";
import ServerLogging from "../shared/logging/ServerLogging";
import {getUser} from "../adalConfig";
export enum SettingsActionTypes {
GET_SETTINGS = 'SETTINGS_GET',
}
export interface ISettingsGetAction {
type: SettingsActionTypes.GET_SETTINGS;
settings: ISettingsState;
}
export type SettingsActions = ISettingsGetAction;
export const getSettings: ActionCreator<ThunkAction<Promise<any>, ISettingsState, null, ISettingsGetAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.settings);
return async (dispatch: Dispatch) => {
try {
const getResponse = await axios.get(url);
const settingsFromServer = getResponse.data as ISettingsState;
settingsFromServer.vehicleTypes.sort((a, b) => a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1);
settingsFromServer.collateralTypes.sort((a, b) => a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1);
settingsFromServer.bodyStyles.sort((a, b) => a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1);
dispatch({
type: SettingsActionTypes.GET_SETTINGS,
settings: settingsFromServer,
})
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Settings",
user: getUser().userName,
url: url,
});
}
}
}
@@ -0,0 +1,74 @@
import { IVehicleFloorPricingParametersState, IVehicleFloorPricingParameters, VehicleFloorPricingParametersPutRequest } from "../reducers/VehicleFloorPricingParametersReducer";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from 'redux-thunk'
import axios from 'axios';
import { toast } from "react-toastify";
import ToastDefaults from "../shared/notifications/ToastDefaults";
import ServerLogging from "../shared/logging/ServerLogging";
import Configuration from "../shared/configuration/Configuration";
import {getUser} from "../adalConfig";
export enum VehicleFloorPricingParametersActionTypes {
GET_VEHICLE_FLOOR_PRICING_PARAMETERS = 'VEHICLE_FLOOR_PRICING_PARAMETERS_GET',
PUT_VEHICLE_FLOOR_PRICING_PARAMETERS = 'VEHICLE_FLOOR_PRICING_PARAMETERS_PUT'
}
export interface IVehicleFloorPricingParametersGetAction {
type: VehicleFloorPricingParametersActionTypes.GET_VEHICLE_FLOOR_PRICING_PARAMETERS;
vehicleFloorPricingParameters: IVehicleFloorPricingParameters[];
}
export interface IVehicleFloorPricingParametersPutAction {
type: VehicleFloorPricingParametersActionTypes.PUT_VEHICLE_FLOOR_PRICING_PARAMETERS;
vehicleFloorPricingParameters: IVehicleFloorPricingParameters[];
}
export type VehicleFloorPricingParametersActions = IVehicleFloorPricingParametersGetAction | IVehicleFloorPricingParametersPutAction;
export const getVehicleFloorPricingParameters: ActionCreator<ThunkAction<Promise<any>, IVehicleFloorPricingParametersState, null, IVehicleFloorPricingParametersGetAction>> = () => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.vehicle_floor_pricing_params);
return async (dispatch: Dispatch) => {
try {
const response = await axios.get(url);
const floorPricingParametersFromServer = response.data;
dispatch({
type: VehicleFloorPricingParametersActionTypes.GET_VEHICLE_FLOOR_PRICING_PARAMETERS,
vehicleFloorPricingParameters: floorPricingParametersFromServer,
})
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Get Vehicle Floor Pricing Parameters",
user: getUser().userName,
url: url,
});
}
}
}
export const putVehicleFloorPricingParameters: ActionCreator<ThunkAction<Promise<any>, IVehicleFloorPricingParameters[], null, IVehicleFloorPricingParametersPutAction>> = (parameters: IVehicleFloorPricingParameters[]) => {
const url = Configuration.PrefixRepoApiUrl(Configuration.Get.urls.repoApi.vehicle_floor_pricing_params);
return async (dispatch: Dispatch) => {
try {
var request: VehicleFloorPricingParametersPutRequest = {
vehicleFloorPricingParameters: parameters
};
const response = await axios.put(url, request);
const floorPricingParametersFromServer = response.data.vehicleFloorPricingParameters;
dispatch({
type: VehicleFloorPricingParametersActionTypes.PUT_VEHICLE_FLOOR_PRICING_PARAMETERS,
vehicleFloorPricingParameters: floorPricingParametersFromServer
});
toast.success('Vehicle Floor Parameters Updated.', ToastDefaults.baseParams);
} catch (err) {
ServerLogging.ResolveErrorWithToast(err, {
action: "Put Vehicle Floor Pricing Parameters",
user: getUser().userName,
url: url,
arguments: parameters
});
}
}
}
@@ -0,0 +1,48 @@
import {AuthenticationContext, AdalConfig} from 'react-adal';
export const adalConfig:AdalConfig = {
tenant: '63cd1554-2783-4a86-ae50-b9e6c21c9499',
clientId: '60f721aa-75bf-4195-ae48-d42cbfd8326b',
endpoints: {
api: "cc253cda-05b2-425c-a60c-d1f516a1c4a9"
},
cacheLocation: "localStorage"
};
export const authContext = new AuthenticationContext(adalConfig);
authContext.handleWindowCallback();
// This is an example of getting the token, or the user.
// To get a users groups, it's getUser().profile.roles
export const getToken = () => authContext.getCachedToken(adalConfig.clientId);
export const getUser = () => authContext.getCachedUser();
const hasRole = (profile: { roles?: string[] }, role:string) : boolean => {
if( !profile || !profile.roles || profile.roles.length <= 0 ) {
return false;
}
let hasRole = false;
profile.roles.forEach( (roleInput) => {
if( roleInput.toLowerCase() === role.toLowerCase() ) {
hasRole = true;
}
});
return hasRole;
};
export const hasAdminRole = () => {
const user = getUser();
return user && hasRole( user.profile, "Admin");
};
export const hasAnyRole = () => {
const user = getUser();
return user && (hasRole( user.profile, "Admin") || hasRole(user.profile, "Specialist"));
};
/*
export const adalApiFetch = (fetch: any, url: string, options: any) =>
adalFetch(authContext, adalConfig.endpoints.api, fetch, url, options);
export const withAdalLoginApi = withAdalLogin(authContext, adalConfig.endpoints.api);
*/
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

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

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