This commit is contained in:
Zeeshaun
2026-03-23 11:21:52 -05:00
parent de9ddd5001
commit a17bc4b4a9
426 changed files with 60574 additions and 1 deletions
@@ -4,6 +4,6 @@
},
"primary": {
"SeqUrl": "https://qa-seq.lcu-internal.com",
"TemenosUrl": "https://lms-test-dmz-lcu.temenoscloudna.com"
"TemenosUrl": "https://lms-test-app-lcu.temenoscloudna.com"
}
}
@@ -0,0 +1,6 @@
node_modules
build
Dockerfile
./junit
junit.xml
src/environments/version.ts
@@ -0,0 +1,30 @@
# Set to true to
root = true
# Indicates that the following rules will apply to all files
[*]
# Choose in between tab or space
indent_style = space
# number of columns used per indentation level
indent_size = 4
# line break representation
end_of_line = lf
# character set
charset = utf-8
# Remove whitespace at the end of lines
trim_trailing_whitespace = true
# Insert a line at the end of a file
insert_final_newline = true
# applies to files with the .md extension
[*.md]
trim_trailing_whitespace = false
# applies to js files inside the src folder
[src/**.js]
trim_trailing_whitespace = true
# applied to package.json and .travis.yml
[{package.json,.travis.yml}]
indent_size = 2
@@ -0,0 +1,64 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"react/prop-types": "off",
"@typescript-eslint/explicit-member-accessibility": [
"off",
{
"accessibility": "explicit"
}
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "semi",
"requireLast": false
}
}
],
"@typescript-eslint/quotes": "warn",
"sort-imports": "off",
"no-console": "warn",
"prefer-const": "warn",
"react/jsx-boolean-value": "warn",
"react/jsx-curly-spacing": [
"warn",
{
"when": "never"
}
],
"react/jsx-equals-spacing": ["warn", "never"],
"react/jsx-key": "warn",
"react/jsx-no-bind": [
"warn",
{
"allowArrowFunctions": true
}
],
"react/jsx-wrap-multilines": "warn",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"semi": "warn"
}
}
@@ -0,0 +1,131 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.cert
# 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/Release
# 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
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# 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
# VSCode environment details
.vscode
# the version file shouldn't be saved
src/environments/version.ts
# output of tests
junit.xml
./junit
@@ -0,0 +1,3 @@
# registry=https://landmarkcu.pkgs.visualstudio.com/_packaging/LCU-Internal/npm/registry/
# always-auth=true
@@ -0,0 +1,4 @@
{
"trailingComma": "none",
"semi": true
}
@@ -0,0 +1,207 @@
# 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.
### [0.1.4](https://landmarkcu.visualstudio.com///compare/v0.1.3...v0.1.4) (2021-12-15)
### [0.1.3](https://landmarkcu.visualstudio.com///compare/v0.1.2...v0.1.3) (2021-12-09)
### Features
- **component:** Adding in RequestToOwnDepartment to Activity ([88b0954](https://landmarkcu.visualstudio.com///commit/88b0954eadd9ff8fce45830284a9642b523ccdc8))
- **CustomWeightedDispersal:** Add Custom Weighted Dispersal form ([f2cee9b](https://landmarkcu.visualstudio.com///commit/f2cee9b6f2fbacf3f8435321c369960045795349))
- **DeadActivityDisplay:** Add Basic Dead Activity display Mirroring Living Activity Display ([e153223](https://landmarkcu.visualstudio.com///commit/e15322329a916099e94d68ad1118268c6832d199))
- **Departments:** exclude inactive ([e9babe4](https://landmarkcu.visualstudio.com///commit/e9babe485cba4846a24d477718acb6f0763f8bc2))
- **ManualActivityForm:** Add Source ID To Manual LIC Referrals ([928adfb](https://landmarkcu.visualstudio.com///commit/928adfba7dccc2b34a45ea9a9f1682a7d2492c06))
### Bug Fixes
- **Department:** fix translate entry spelling ([fa029b0](https://landmarkcu.visualstudio.com///commit/fa029b07212f7c1dcb5de1ca185d26e890813355))
- **Dispersal:** Require 100% ([d803f08](https://landmarkcu.visualstudio.com///commit/d803f085f93dd77951f0cc1adbedd42214c8f7c1))
- **Dispersal:** restrict dispersal to goal products ([cdcd83e](https://landmarkcu.visualstudio.com///commit/cdcd83eca0ed0f0319acfbe4fb4cb43c70dc4293))
- **EditDepartment:** fix spelling error ([de3b2e0](https://landmarkcu.visualstudio.com///commit/de3b2e06a33bfcba1da8eb461f65306f7fdb2b13))
- **NewActivity:** remove account new balance from form ([710ab97](https://landmarkcu.visualstudio.com///commit/710ab977bcf3862637b5bfd560d25b8f50a6eec0))
### [0.1.2](https://landmarkcu.visualstudio.com///compare/v0.1.1...v0.1.2) (2021-11-23)
### Features
- **Auth:** ask server for auth ([afd7440](https://landmarkcu.visualstudio.com///commit/afd74404633f237031f2a6c9dbb1b98efb0f2bed))
- **Departments:** hide deactivated, update org nbr ([b3128e5](https://landmarkcu.visualstudio.com///commit/b3128e5ee985e7456807029153e85ef272cc4401))
- **Goals:** Import, Export all goals ([e26f573](https://landmarkcu.visualstudio.com///commit/e26f57329fbfd7dc8678b97c0caf6a63b3763c58))
### Bug Fixes
- **Dashboard:** employee dashboard show name ([025fdfd](https://landmarkcu.visualstudio.com///commit/025fdfd78ab73dc430dfa5bc52573d800b3bc6dc))
- **EoE:** fix index on eoe page ([af12cd6](https://landmarkcu.visualstudio.com///commit/af12cd6efe76801f6c3d7eb67287deecb3567d1a))
- **ManualActivity:** less required fields for referrals ([0fec644](https://landmarkcu.visualstudio.com///commit/0fec6447e9e041b0577f9d9812c37fc40c489e53))
- **NewAccountBalance:** remove from display ([7d1b16f](https://landmarkcu.visualstudio.com///commit/7d1b16fa7b608114727ba375cc0783414343afa2))
### 0.1.1 (2021-11-08)
### Features
- **Activity:** Add activity reducer, action, store for form ([a50ade1](https://landmarkcu.visualstudio.com///commit/a50ade1985c2c1a557bea35b59ae752c1b3a1730))
- **Activity:** clean up activity table ([5b2d1a3](https://landmarkcu.visualstudio.com///commit/5b2d1a37377ead4086de37e78ac64ea53883e698))
- **Activity:** Display user ([0807711](https://landmarkcu.visualstudio.com///commit/0807711f27a4bcb6f89fe2c54eac7d64e8ce2967))
- **Activity:** filter activities ([f7971b8](https://landmarkcu.visualstudio.com///commit/f7971b88ab8e2aea76b0e21eded4866433ca2ee3))
- **Activity:** manual entry validation ([1561d67](https://landmarkcu.visualstudio.com///commit/1561d67b136cf0bac15b57387ed9089c4ea2a61f))
- **Activity:** search and sort audit page ([017aeb2](https://landmarkcu.visualstudio.com///commit/017aeb2a52b62cdb6b773ddeb715a001a69b434f))
- **Activity:** split into activity view and form ([ae28fcf](https://landmarkcu.visualstudio.com///commit/ae28fcf56fbedc1553e79e8b5237d2b5482b6f90))
- **Activity:** Update Activity Reducer/Action/State/Service to handle Pagination. Add Caching. ([a1af652](https://landmarkcu.visualstudio.com///commit/a1af65246e1b12c2064a8f75109347e72e9ef498))
- **ActivityAuditLog:** Add Subject Org Nbr And Center Activity Audit Log ([62bb3b5](https://landmarkcu.visualstudio.com///commit/62bb3b5f7c421fb510b63f95c2bd0d054cb411c5))
- **ActivityDetailPage:** Add page for single activity ([9415aa6](https://landmarkcu.visualstudio.com///commit/9415aa618c1c25725f22fe3f2560e1d692a8efc5))
- **ActivityDetailPage:** Move loading message ([b62577d](https://landmarkcu.visualstudio.com///commit/b62577dc479fe1886c6ed39eec221555696a89f4))
- **ActivityDetailsForm:** Wire ActivityDetailsForm to wizard and redux ([033a53c](https://landmarkcu.visualstudio.com///commit/033a53ca1d24ed29696cde013c21f6ab2cceaa59))
- **ActivityDetailsPage:** Add approval buttons for activity state ([172d1d6](https://landmarkcu.visualstudio.com///commit/172d1d6d4a1f9e7245d47e7701d25ab1198ed538))
- **ActivityEditor:** Select Department ([67bc2c6](https://landmarkcu.visualstudio.com///commit/67bc2c669ba863803d533836937742c9f59b0bcf))
- **ActivityProductform:** Wire up activity product form to wizard and redux ([1942686](https://landmarkcu.visualstudio.com///commit/1942686e9b07c87d915de92bd39edfc7882d99b3))
- **ActivityProductForm:** Restrict Product-ActivityType combinations ([db4b56a](https://landmarkcu.visualstudio.com///commit/db4b56aa1deaa5d74907d7c9508dd5dc56dfa991))
- **ActivitySourceForm:** Wire ActivitySourceForm to wizard and redux ([d385551](https://landmarkcu.visualstudio.com///commit/d385551f984ed8c3b9b5ad026b1315c514bc83c6))
- **ActivityTable:** Add Activity Auto-Table ([d35c3c8](https://landmarkcu.visualstudio.com///commit/d35c3c8fd61e903874bf62fd2e7670615d0bba4e))
- **ActivityTransferApproval:** Add component to approve activity transfer ([85ca39f](https://landmarkcu.visualstudio.com///commit/85ca39f083a206186c329e696cde0815368b9f91))
- **ActivityTransferRequest:** Add form to request activity transfer ([3ffac6e](https://landmarkcu.visualstudio.com///commit/3ffac6ef721c418b27985b0ad0b27e7dc2f40d31))
- **AdditionalPermissionsForm:** Add Additional Permissions Form ([5d03b89](https://landmarkcu.visualstudio.com///commit/5d03b89bba892ae48ebcbc64b450101d0b27724b))
- **Admin:** can force status change ([dc809d5](https://landmarkcu.visualstudio.com///commit/dc809d5c6a7d0d971ef87db46ec87c41a11a2a54))
- **BusinessLineTable:** Add business line display to organization overview ([4a5ba10](https://landmarkcu.visualstudio.com///commit/4a5ba109ecbe6933786ec7c8728e7dd4528d4e27))
- **CommissionPage:** Add Commission Page, Reducer, Actions ([bfefac4](https://landmarkcu.visualstudio.com///commit/bfefac468fcf63ebdfb12aaeccfe6da81ea5dc77))
- **CommissionPage:** Add rate editor and audit table ([08a4502](https://landmarkcu.visualstudio.com///commit/08a45024641077d666c00b87ac93c866dd741cfc))
- **dashboard:** show all departments ([123ffe3](https://landmarkcu.visualstudio.com///commit/123ffe3e9a019be9575cf7d948da2e41308ba0f7))
- **Dashboard:** add new dashboards ([a9b7cdf](https://landmarkcu.visualstudio.com///commit/a9b7cdf8861944d31e2a552304605b19969b18ad))
- **Dashboard:** dept dash for all ([844d79a](https://landmarkcu.visualstudio.com///commit/844d79a37c3aa38bd30f14cdf745b711b277fd5c))
- **Dashboard:** determine dashboard ([70035a6](https://landmarkcu.visualstudio.com///commit/70035a61e48d9ac9403a15991d0c77c83c456e22))
- **Dashboard:** region to department ([88c2bb1](https://landmarkcu.visualstudio.com///commit/88c2bb1a30dc620d4b919f71b3b5f1101073cbc7))
- **Dashboard:** Start dashboard ([e8f15a6](https://landmarkcu.visualstudio.com///commit/e8f15a69eddde7e4d7c6862b29701d9d3f05e349))
- **DebtProt:** show as if it had goals ([f7b1371](https://landmarkcu.visualstudio.com///commit/f7b1371238c47d7e56ded3ff85cfbc16e9ee57f7))
- **Decision:** Update State Liist ([60ca6f8](https://landmarkcu.visualstudio.com///commit/60ca6f8aa64aeaf786fd96622e55f8f75a930130))
- **Department:** Add Department Model, Service, Reducer, Actions ([f79feb1](https://landmarkcu.visualstudio.com///commit/f79feb189ec8e3e6876ce6e08eb2dca2151014f8))
- **Department:** detail page stub ([76ce51d](https://landmarkcu.visualstudio.com///commit/76ce51d8d18177ad6739a296711a7d086dfacee9))
- **DepartmentAssignmentAlertBar:** Add Notification for Submiting Auction Assignment ([6f2b724](https://landmarkcu.visualstudio.com///commit/6f2b724552d20598a11bbdd24e3311c20dd4e4fd))
- **DepartmentAssignmentTabGroup:** Add Tab Group for Audit and inActive Assignments ([6fcc360](https://landmarkcu.visualstudio.com///commit/6fcc360ad20da8b60a2fdec00e1fdf3133727e90))
- **DepartmentDetail:** Add component to display department details ([97bcd1f](https://landmarkcu.visualstudio.com///commit/97bcd1fe986073f90842ec95dfc4601bee8615ae))
- **DepartmentGoalsEditor:** Add export to csv ([28d1add](https://landmarkcu.visualstudio.com///commit/28d1addbda64245d7ab7e30639f73fb1629d49f7))
- **DepartmentTable:** Add basic display for departments ([86e81fa](https://landmarkcu.visualstudio.com///commit/86e81fafb9d9c710c629ba4a71e3d52f98c3627f))
- **DepartmentTable:** Add region selection to department form ([e4ca89c](https://landmarkcu.visualstudio.com///commit/e4ca89c1e3844e86a6304ff3fa29b652e8c6c22a))
- **DepartmentTableRow:** Add Update Form to Department Table Row ([5eb0a51](https://landmarkcu.visualstudio.com///commit/5eb0a51f767f1ce6c596f5329cc7b2fdf82378bc))
- **DeptAssign:** Role based departments ([6a006f1](https://landmarkcu.visualstudio.com///commit/6a006f1257107c94f6883a5cfb85820ea73f3045))
- **Detail:** make note a link ([5496ad2](https://landmarkcu.visualstudio.com///commit/5496ad2d3f02116a0a04c716b11dc176badc1c52))
- **DispersalForm:** Add dispersal form and displays ([c0e9e6d](https://landmarkcu.visualstudio.com///commit/c0e9e6d706725964161babe50d3bd94452d26610))
- **Employee:** Search for employees ([7b46dae](https://landmarkcu.visualstudio.com///commit/7b46dae5a6171598ca707e2e3c8735cb6839ffe0))
- **EmployeeDashboard:** Add component to select month range ([5879750](https://landmarkcu.visualstudio.com///commit/58797500bd27915ba645ba540d7845b1d9b001ea))
- **EmployeeDashboard:** Add progress views for user's departments ([f8877ab](https://landmarkcu.visualstudio.com///commit/f8877abc0a0be8cad1dde3e34f364152e74ce245))
- **EmployeeDashboard:** Update personnel goals and activities to use month range ([1dd3bad](https://landmarkcu.visualstudio.com///commit/1dd3bad680ab69509719ac3d7b2ab2dd44310462))
- **EmployeeDashboardPage:** Add alert for regional manager without a region ([c942408](https://landmarkcu.visualstudio.com///commit/c94240815d5823db8fead4c6543d6ae22d6a69be))
- **Employeepage:** Add initial employee models and a route with parameter ([9f63a8a](https://landmarkcu.visualstudio.com///commit/9f63a8a3be1cdd2aee35500fb172d43bd3fddaf3))
- **EmployeePage:** Add Employee Page, Reducer, Action ([eace35c](https://landmarkcu.visualstudio.com///commit/eace35cf16a7f28fd8d1e8ab7c3da97b2168d4cc))
- **eoe:** supply termed employee list ([2a71148](https://landmarkcu.visualstudio.com///commit/2a711486725a57c01113d2a03775489fca7db48f))
- **GoalDispersalForm:** Add Goal Dispersal to Personnel Goals ([ec3b176](https://landmarkcu.visualstudio.com///commit/ec3b176fe26d16b326e95cb2b53bdd9c39e0fb4c))
- **GoalEditor:** Personnel goals must add up to department ([a9f4260](https://landmarkcu.visualstudio.com///commit/a9f426099806b3e70c47d0fd5b753aed4effdca0))
- **GoalEditors:** show goal details ([c931357](https://landmarkcu.visualstudio.com///commit/c931357d171ca9cccacbaa361bd2bddb663cbc03))
- **GoalReducer:** Create Goal Actions, Reducer, Redux Store Entry ([ad58c82](https://landmarkcu.visualstudio.com///commit/ad58c8292e2a450b356b28adcc4df61d616cc083))
- **Goals:** display basic progress and audit ([60fb43a](https://landmarkcu.visualstudio.com///commit/60fb43ab2600af72c662627a2e4aebb4edb20551))
- **Goals:** Manage goals, fix months ([d234144](https://landmarkcu.visualstudio.com///commit/d234144ba2c9aec4a0d2fe1f0fecdbe856261c74))
- **Goals:** Personal Dept summary ([23edbf7](https://landmarkcu.visualstudio.com///commit/23edbf73803415fe9295288c693949217f52d6aa))
- **Goals:** Save department goals ([d134f12](https://landmarkcu.visualstudio.com///commit/d134f12545c1add67320833b40f80d24ea724def))
- **Goals:** Show editor totals ([23b3bf4](https://landmarkcu.visualstudio.com///commit/23b3bf404bbfa9af9c1bd41d4a50eefd5b1ee5f1))
- **Goals:** Show personnel goals ([e070cb4](https://landmarkcu.visualstudio.com///commit/e070cb44ae86e12e309b05456062d80567e830f7))
- **Goals:** Use real departments ([e718a96](https://landmarkcu.visualstudio.com///commit/e718a9645285599179d0ff51c1703463621cfc7f))
- **Goals:** View Department Goals ([fc45182](https://landmarkcu.visualstudio.com///commit/fc45182e9cac77ba5d3c011c0d3e7d87a7836bcd))
- **GoalsPage:** Add GoalsPage to retreive state and GoalDisplay to display list ([7007c0f](https://landmarkcu.visualstudio.com///commit/7007c0f293648540e496d25b4ea9de7cd3bce01e))
- **Layout:** Add admin navigation, Footer fix ([e310f82](https://landmarkcu.visualstudio.com///commit/e310f823623f785dc93a8dcb3e4d8f7d79f3da9c))
- **ManualActivity:** Add New Fields to Manual Activity Form ([b2fb8e9](https://landmarkcu.visualstudio.com///commit/b2fb8e98aee8cff1d10e361bc2ddc26613568bd3))
- **ManualActivityForm:** Add base inputs to wizard with dropdown options ([1f1dfdb](https://landmarkcu.visualstudio.com///commit/1f1dfdb3b9bd9ea2013fcfcd918203b956c16da3))
- **ManualActivityWiard:** Wire wizard submit and clear, Add Notification bar ([8ea370d](https://landmarkcu.visualstudio.com///commit/8ea370d5bc1f9f5234c5add694a0c13d11a57498))
- **ManualActivityWizard:** Clear form data on successful submit to avoid duplication ([7b910d2](https://landmarkcu.visualstudio.com///commit/7b910d23033fc8e84358aa10cee7d67eb6d74f0b))
- **MSAL:** Switch from ADAL to MSAL ([19f3f42](https://landmarkcu.visualstudio.com///commit/19f3f42b1a67cdbb1f1e4274a5116255281d89d8))
- **Navigation:** Add User Information to Nav Bar ([06d25d5](https://landmarkcu.visualstudio.com///commit/06d25d5cc97d22f70724565fd3454a2b1eb95495))
- **NewDepartmentAssignmentRow:** Add very basic add form to department assignment table ([3fc10f1](https://landmarkcu.visualstudio.com///commit/3fc10f15132380063fa25dfd9014518521e8de6f))
- **OrganizationView:** Add admin org view ([e1db531](https://landmarkcu.visualstudio.com///commit/e1db53142a177e57ef97036afb28b26bb2d0fae3))
- **PagedContentCotrols:** Add paged content controls ([3d9b59e](https://landmarkcu.visualstudio.com///commit/3d9b59e8d29d7901875eb6aad5e7be41abdf271e))
- **PayDay:** show next bonus payday ([e325bd4](https://landmarkcu.visualstudio.com///commit/e325bd4f439a4d8b50e7053d369c0ac6cbab5be5))
- **Payout:** Show eligible payout profiles ([020c082](https://landmarkcu.visualstudio.com///commit/020c0825e5f35bd327e885f3c630eb4112ac89c9))
- **Payouts:** Add basic payout display ([92fb7eb](https://landmarkcu.visualstudio.com///commit/92fb7eb5668f27a9fc6599656e8b4cbca92b8995))
- **Payouts:** export summary items ([a4e1be0](https://landmarkcu.visualstudio.com///commit/a4e1be0a6340bef9ac3bf88eb444c4c9f46422bd))
- **Payouts:** load and export payouts ([69bb552](https://landmarkcu.visualstudio.com///commit/69bb552343931c0574c15ea6178be633fa1a2f4c))
- **Payouts:** Payout on employee profile ([d69766f](https://landmarkcu.visualstudio.com///commit/d69766ff03e7e059b602c7084d7f55336c369c0a))
- **Permissions:** Display permissions ([c1033ed](https://landmarkcu.visualstudio.com///commit/c1033ed69bf38d5f933f9edc0c6f400a3125c3ec))
- **PersonnelGoalsEditor:** Add export to csv ([b7dfd24](https://landmarkcu.visualstudio.com///commit/b7dfd244402815695516a04d70bc7383059ec5d3))
- **PersonnelGoalsEditor:** Add option to toggle showing all personnel ([ae59e83](https://landmarkcu.visualstudio.com///commit/ae59e837e7db929137d31f749690c1523e256c38))
- **Product2:** Add Debt Protection Elligible Loan Product ([25af51c](https://landmarkcu.visualstudio.com///commit/25af51c2c9b2772b44146e95e61c00d845294b33))
- **Profile:** Add basic profile ([71dd308](https://landmarkcu.visualstudio.com///commit/71dd308bd45851d890ecb908104193109c89bd63))
- **Profile:** Add profile link ([ed31530](https://landmarkcu.visualstudio.com///commit/ed315308a2e9eb89c294c6c42e48cc7c4195e69e))
- **Profile:** Combine Profile ([4cef97d](https://landmarkcu.visualstudio.com///commit/4cef97dc5bc3ac3696a6806fb09e6e2ed1ecc791))
- **Profile:** show commissions ([efaf24d](https://landmarkcu.visualstudio.com///commit/efaf24d3f4c513e7a54e3bac860a86836eb9e670))
- **Profile:** Temporary profile components ([0cb4cd2](https://landmarkcu.visualstudio.com///commit/0cb4cd2300f9f5d3a8d0721a42d71782a8903011))
- **Progress:** show sales and referrals ([8c68df6](https://landmarkcu.visualstudio.com///commit/8c68df687477763ae16a3cab521298b59705c910))
- **Region:** Regional dashboard ([3bda580](https://landmarkcu.visualstudio.com///commit/3bda580053515fa24b40caf606d7ecd7f8027885))
- **RegionTable:** Add form to set regional manager ([95d481c](https://landmarkcu.visualstudio.com///commit/95d481c7f396b98a39ef67d41c3a6384852aa934))
- **RegionTable:** Add region table ([64ad89d](https://landmarkcu.visualstudio.com///commit/64ad89d38a397ed63a4648d580aa697e6c475a0e))
- **Roles:** Rank roles, restrict assignments ([fc5f15a](https://landmarkcu.visualstudio.com///commit/fc5f15a013499c13df23411cefbe36f6a7aac41f))
- **RoleTable:** Add Role Table and Role Management Page ([7c70510](https://landmarkcu.visualstudio.com///commit/7c70510ef45848b99cd73a8f28db814fb9f0784f))
- **Search:** search for users ([602bf13](https://landmarkcu.visualstudio.com///commit/602bf138d57077bab8327626749c852d07a0a41d))
- **Security:** Basic permission guard ([93d823a](https://landmarkcu.visualstudio.com///commit/93d823a8e7021a75ac2cbb31fa2879bfab78c28b))
- **Settings:** Add Settings Reducer, Actions, Store, Service ([a4bb37e](https://landmarkcu.visualstudio.com///commit/a4bb37ec16772cb3a854e30933741eb9bf1b2da1))
- **Tables:** center some columns ([ddde5f3](https://landmarkcu.visualstudio.com///commit/ddde5f3ee2e9b70e23c877727b22e1e4437d787a))
- **UserDept:** Role per dept, sliding payout ([e0f50b8](https://landmarkcu.visualstudio.com///commit/e0f50b84bd506093fd7f80c13d63d5eb1b884f9e))
- **UserSearchSelect:** Add control to search and select person ([d7f418b](https://landmarkcu.visualstudio.com///commit/d7f418bb54ba266f8e0d4cfbf3117d6b0044bebb))
- **Validation:** conditional required fields ([525d0ff](https://landmarkcu.visualstudio.com///commit/525d0ff075d4da477743fc26b294479f99276158))
### Bug Fixes
- **Activity:** Allow cents ([21b9db2](https://landmarkcu.visualstudio.com///commit/21b9db2524fe7da3b1ffad9ea7abbe90b7f4bb76))
- **Activity:** manual entry bugs ([8fb6f68](https://landmarkcu.visualstudio.com///commit/8fb6f683d60133ecb71282d6f6f7458fadf433f9))
- **Activity:** note for state change ([cc14b0d](https://landmarkcu.visualstudio.com///commit/cc14b0d421d8ea1c375e5fbf0c1b0b37fe6ac1f3))
- **Activity:** save correctly, correct grammar ([68f641c](https://landmarkcu.visualstudio.com///commit/68f641c4303e59cf40b7ee7114f0582b9f097f87))
- **ActivityAudit:** some columns do not sort ([9093b25](https://landmarkcu.visualstudio.com///commit/9093b253cae5d339e6c70d062584635be099820c))
- **ActivityDetailDisplay:** Fix missing fields in Activity Detail ([adf3b60](https://landmarkcu.visualstudio.com///commit/adf3b60c9aed24dbfe14b56982ce0726a96a0d63))
- **ActivityDetailPage:** Update state change handling logic ([6f949cc](https://landmarkcu.visualstudio.com///commit/6f949ccdd552391babac22bf22d1b5ee4244a45f))
- **ActivityService:** Fix URL parsing/formatting bug ([69d879f](https://landmarkcu.visualstudio.com///commit/69d879f79e6e10dc56312579ad8f9efd146278c8))
- **ActivityTable:** Fix Activity Table Headers ([d75c6b5](https://landmarkcu.visualstudio.com///commit/d75c6b5b115ed940d2dc290915a5da83bb9e7e57))
- **appsettings:** remove localhost ([f35f6da](https://landmarkcu.visualstudio.com///commit/f35f6dac40cc8262d93041941d9ec92020792d31))
- **BasicDeptAssignmentTable:** Fix time format string ([c62a552](https://landmarkcu.visualstudio.com///commit/c62a552b993f1193925b520b407d066172fcd678))
- **CommissionPage:** Display timestamps in local time ([5a8ad15](https://landmarkcu.visualstudio.com///commit/5a8ad15ba966ebe0eca515b76d9e4e1ad024a225))
- **CommissionRatesEditor:** Fix logic to detect pending changes ([b58a487](https://landmarkcu.visualstudio.com///commit/b58a487883dd614a3b3b14aa3a5ea995da8a4c87))
- **CommissionRatesEditor:** Include roles with commissions ([9faec49](https://landmarkcu.visualstudio.com///commit/9faec497840791de6b455113b6285c06aec01a0b))
- **CommissionTable:** Sort by updated date descending ([42fc1a2](https://landmarkcu.visualstudio.com///commit/42fc1a2a7293771fab73101290d24412c2807d0f))
- **Dashboard:** remove route props ([a7c495a](https://landmarkcu.visualstudio.com///commit/a7c495ac6c021ef59a2d9d5fb2b7e6afda02afb2))
- **DebtProtection:** Correct dp totals ([8ad0ef6](https://landmarkcu.visualstudio.com///commit/8ad0ef60d103074491f187314df5ed66a95abafe))
- **Departments:** use provider! ([ba65661](https://landmarkcu.visualstudio.com///commit/ba65661f2fcce8b5ae14342a788dabb0e91503b7))
- **DeptAssign:** Submit UTC Timestamps ([ab86a1b](https://landmarkcu.visualstudio.com///commit/ab86a1ba79ec35b776d4ce6bb753891cf830585e))
- **DeptAssign:** Use correct end date field ([4a35bb2](https://landmarkcu.visualstudio.com///commit/4a35bb2c2a3e126e318b46e91d1c4b6376daa0df))
- **Editor:** Wait for departments ([e66cf08](https://landmarkcu.visualstudio.com///commit/e66cf08dcde5b53690c34361b1f8b50e25916f67))
- **EmployeeSearch:** missing permission ([9d36c9c](https://landmarkcu.visualstudio.com///commit/9d36c9c06b3902eb8383f5edcb7d01c7c40b0ca1))
- **EoE:** add 1 to month ([f1aef06](https://landmarkcu.visualstudio.com///commit/f1aef06c46173314a9e3a1a91e227199886c799b))
- **Feedback:** Rename button, nesting doll, env ([5e42b16](https://landmarkcu.visualstudio.com///commit/5e42b16111bcf7bcdc93559cea281862574d3de0))
- **Form:** cannot set value? ([1eb64ed](https://landmarkcu.visualstudio.com///commit/1eb64ed86a76ba0548de25da50fcc1b4f4d10f5a))
- **GoalEditors:** correct tab name to details ([0b330db](https://landmarkcu.visualstudio.com///commit/0b330db7dd9524aa770f3ef06824fcd3fcc8d463))
- **Goals:** Allow 999 ([6dc6837](https://landmarkcu.visualstudio.com///commit/6dc6837f7c6c9e608f390681d8b8a135b6d318de))
- **Goals:** Allow zeroes ([d5cfe30](https://landmarkcu.visualstudio.com///commit/d5cfe308b63855bc2dd3e31e087e5a4085a0356b))
- **Goals:** include additional perms ([a78d4b9](https://landmarkcu.visualstudio.com///commit/a78d4b96a0ea56e050d6ee0b45f04be04b71e88b))
- **Goals:** Show updated in utc ([a52ed06](https://landmarkcu.visualstudio.com///commit/a52ed061aebc1a5bc3b1158c85e0e15e046b911c))
- **Goals:** Use employee not string ([125814e](https://landmarkcu.visualstudio.com///commit/125814e057014399bdafcdc42dbc9886fb8eb526))
- **GoalTotals:** display 0 instead of nan ([42247f1](https://landmarkcu.visualstudio.com///commit/42247f104a9601f188c5fa9245b8ef7c616ae184))
- **Jest:** Switch from ejcted jest tests to CRA tests with reporters ([0395f58](https://landmarkcu.visualstudio.com///commit/0395f585380d37fa755d2e92e5fe21de14d70f9e))
- **LIC:** show Lic as LIC ([41ca697](https://landmarkcu.visualstudio.com///commit/41ca697538a8a1d8b4231ebc0490579eaca6315c))
- **ManualActivityWizard:** Enable datetime inputs, submit with credentials ([5568603](https://landmarkcu.visualstudio.com///commit/5568603a8599ec9a41817fee86dc1ae9ac8f34d0))
- **ManualActivityWizard:** Map empty string to null, cast balance to number ([44aa06d](https://landmarkcu.visualstudio.com///commit/44aa06defb341567886a6b3f377befb77f01a87f))
- **MSAL:** url and log ([7ae8ebe](https://landmarkcu.visualstudio.com///commit/7ae8ebe79ce8d9307bd5e0f4327f41ffecaf839f))
- **Multi:** department work ([8a5cd58](https://landmarkcu.visualstudio.com///commit/8a5cd58ba6af55eba576ef797ea9e6aec337e648))
- **Multi:** fix 22711,22999,21785 ([a6fb13f](https://landmarkcu.visualstudio.com///commit/a6fb13f85dc101ee5da9b952e6eeff11adf07a56))
- **Navigation:** Fix navbar navigation by prefixing basename ([a6158ba](https://landmarkcu.visualstudio.com///commit/a6158ba36304750c538a33b61b048afcf5b448cc))
- **Payout:** allow partial payout ([a7555db](https://landmarkcu.visualstudio.com///commit/a7555db37c1d3e3e91e1f74cb506894a0db12525))
- **Permission:** get active departments ([11221a3](https://landmarkcu.visualstudio.com///commit/11221a31f4a020ff36330028d323feec90352f87))
- **Permission:** remove all permission ([5485f89](https://landmarkcu.visualstudio.com///commit/5485f896df2d98a71ee56591160b368935073968))
- **Permissions:** do not center permissions ([56e181c](https://landmarkcu.visualstudio.com///commit/56e181cc82b4c01af8b7188e5780bc0328581dc1))
- **PersonnelGoalsEditor:** Include employees with set goals ([728913f](https://landmarkcu.visualstudio.com///commit/728913f99de9619f6100d62cb54ef66f97749b40))
- **Profile:** keep current user profile loaded ([62fa6e4](https://landmarkcu.visualstudio.com///commit/62fa6e485e20f121827072112d1a455d927cd7cf))
- **Profile:** re-order column, fixes 23676 ([c09db52](https://landmarkcu.visualstudio.com///commit/c09db521ac22d18a05d19217504ef7e9c182e480))
- **Profile:** remove oops ([0801c8a](https://landmarkcu.visualstudio.com///commit/0801c8a42f282d11b32b6c422b361901f34f4eb9))
- **Profile:** show full name from dayforce ([69ea7d9](https://landmarkcu.visualstudio.com///commit/69ea7d9dba1a305db5ce0950bc927851537aa611))
- **Role:** handle null role ([264636d](https://landmarkcu.visualstudio.com///commit/264636d386d6f329a4bcfd768b508fa6a00faa94))
- **Role:** use role from department assignments ([4f4ed0f](https://landmarkcu.visualstudio.com///commit/4f4ed0f046978905fb1d0b64fc3deef7940670b9))
- **Router:** Remove permission guard wrapping a route ([b668176](https://landmarkcu.visualstudio.com///commit/b668176772f42c49c0a9102b89e7bbcaa19362f9))
- **Settings:** update all useSelectors ([9340b2d](https://landmarkcu.visualstudio.com///commit/9340b2d16bdd35f4ca55651d68e5599e342f9084))
- **Spelling:** fix some grammar up ([1689051](https://landmarkcu.visualstudio.com///commit/1689051e2c95b8fea3203ee7361e91be3b66ccee))
- **Tests:** Add receipts to tests ([47ff204](https://landmarkcu.visualstudio.com///commit/47ff204298bff30be210886471d69f69608de987))
- **TimeStamp:** Display in local time ([611519c](https://landmarkcu.visualstudio.com///commit/611519c565735211a6e88d41e41bd31108e24cf5))
- **Translate:** Fix merge error ([cba901e](https://landmarkcu.visualstudio.com///commit/cba901e29c3d826a11039e204732750365db5248))
@@ -0,0 +1,62 @@
FROM node:12-buster AS node-install
# Fix EOL apt sources and install git
RUN set -eux; \
cp /etc/apt/sources.list /etc/apt/sources.list.dist || true; \
printf '%s\n' \
'deb http://archive.debian.org/debian buster main contrib non-free' \
'deb http://archive.debian.org/debian buster-updates main contrib non-free' \
'deb http://archive.debian.org/debian-security buster/updates main contrib non-free' \
> /etc/apt/sources.list; \
echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-check-valid-until; \
apt-get -o Acquire::Check-Valid-Until=false update; \
apt-get install -y --no-install-recommends git ca-certificates; \
rm -rf /var/lib/apt/lists/*
RUN git --version
# Project env (skips version.js in postinstall per your comment)
ENV ENV_IS_DOCKER=true
WORKDIR /app
COPY package.json package-lock.json isDockerEnv.js ./
# ---- Optional: Azure Artifacts auth
# Pass these in your pipeline only if you need the private feed:
# --build-arg FEED_URL=https://pkgs.dev.azure.com/<org>/<project>/_packaging/<feed>/npm/registry
# --build-arg PAT=$(System.AccessToken) # or a PAT with Packaging read
ARG FEED_URL=""
ARG PAT=""
RUN set -eux; \
if [ -n "$FEED_URL" ] && [ -n "$PAT" ]; then \
host="$(echo "$FEED_URL" | sed -E 's|^https?://||')"; \
printf 'registry=%s/\n//%s/:_authToken=%s\nalways-auth=true\n' "$FEED_URL" "$host" "$PAT" > .npmrc; \
fi
# Install deps and ensure node-sass binary matches Node 12
RUN npm install --before=2025-09-1 --unsafe-perm \
&& npm rebuild node-sass || true
# ---- Build
FROM node-install AS node-build
#Copy React Project Files
COPY . .
#Generate version.ts file and run production build
RUN node version.js && npm run build
FROM node-build AS node-test
#Set label to reference this container from pipeline to get test results
LABEL test=true
RUN npm run test-ci;
#Nginx layer for serving build
FROM nginx:stable-alpine AS nginx
#Set working directory to default nginx html directory
WORKDIR /usr/share/nginx/html
#Remove default assests from base directory
RUN rm -rf ./*
#Copy build folder to nginx working directory
COPY --from=node-build /app/build .
#Copy Nginx Config From Project Files.
COPY --from=node-build /app/nginx.conf /etc/nginx/conf.d/default.conf
ENTRYPOINT ["nginx","-g","daemon off;"]
@@ -0,0 +1,290 @@
# Incentive Manager
React/Redux client using IncentiveAPI to manage incentives and display progress for associates, branches and regions.
# Getting Started
If you haven't set up your local environment for react development yet clone this repo and run the setup script to
install the required tools [here](https://landmarkcu.visualstudio.com/Platform/_git/LCU.Setup.Internal.EnvironmentSetup?version=GBmaf%2FinitialCommit)
Once that has finished you can run the SetupLocalSSL script in the root directory of the project,
follow the prompts to install a local SSL cert for your project.
Once that is complete you can run the "start-https" npm script and begin adding your feature
# Style Concerns
ESLint and Prettier are the managers for code style. You may need to add an extension to your code editor for both Eslint and Prettier. There is a git commit hook setup to auto-apply Prettier rules after each commit. All edited files will be cleaned up.
On top of these linters we do have a few preferences that you should keep in mind:
- React - Functional Components Only, API Calls in Services
- Testing - Enzyme/Jest
- Forms - Conform to outlined standards
- Releases - Handle in Azure DevOps
- Commits - Commitizen style
- ADA Compliance
## React
**Functional Components Only**
In React 16.8 hooks were added to functional components giving them the same functionality as class components. These new functional components are much easier to read and do not require the 'Connection' declaration that class components did.
Learn more about react hooks [here](https://reactjs.org/docs/hooks-overview.html)
Learn more about using Typescript with react hooks [here](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks/)
**API Calls In Services**
To better keep our react projects DRY we move API calls to services instead of using them in actions or react hooks. This allows us to easily update all API calls throughout the entire application safely should an API change.
You can find services in `src/shared/services`
## Testing
Enzyme and Jest are the testing libraries.
- https://devdocs.io/enzyme/
- https://devdocs.io/jest/
## Forms
While forms have various roles and requirements, there are a set of standards that we can agree to when approaching form
creation. Please keep these standards in mind as a guide and not a hard rule - although variations should likely be discussed with the team.
- Disable submit for invalid forms, ensure 'enter' doesn't bypass this.
- Mark required fields with a red \*
- Prefer 'immediate' form feedback
- Do not store form state in redux
- Use React-Strap components when possible: https://reactstrap.github.io/components/form
- Use the React-Strap form validation UX:
- Outlined fields in Green/Red
- Interior Green/Red icon
- Message below input
- You may include a success message when appropriate
- https://reactstrap.github.io/components/form/#Form-Validation
- Technical Recommendations:
- Fields should have basic validation 'locally' in their component
- Form-Wide validation should be handled in a parent component
- When possible, store validation functions separately in the shared folder for re-use
- Present success or failure in a banner above or below the form
- Server feedback should be presented in a way that the appUser can either correct the form or provide a useful bug report
## Connected Vs. Components Vs. Layout - Where does this component go?
React Components in this project are broken into 3 folders under the src directory _Connected_, _Components_, and _Layout_.
On first glance there is practically no difference between the react components saved in each. The distinction comes from its role within the application and its relationship to the redux store.
**Connected - The Redux Folder**
A _Container_ is a concept recommended by the Redux team to isolate app state from the display logic. In this project the folder is renamed _Connected_ as a 'container' is a lose term and can be applied to unconnected react components as well.
_Connected_ components are the 'pages' that contain non-connected components that depend upon data from the Redux Store. More specifically, _Connected_ components are top level components that hold Redux functionality (such as Selectors and Dispatchers) and distribute it to child display components.
Anything that uses a Selector hook or several Dispatch hooks should be saved in the _Containers_ folder and should not display any of its own data.
**Components - The React Folder**
The _Components_ folder is where the bulk of the React Components should be saved. The _Components_ folder should contain the lowest level and most specific components. They are unconcerned with where their props come from and are in charge of collecting or displaying a piece of state.
None of these components should use a Selector (arguments can be made for allowing Dispatch, this project tends to avoid it), instead, the data will flow into these components from the app state via a _Connected_ component's Selector. The final stop for all data is always a _Component_.
Glue components, or components that group together multiple children components, help transform data as well as manage state between several display components. While their function is similar to that of a Connected Component, glue components belong in the _Component_ folder, as they are not dependent on the Redux store. Keep in mind glue components are not explicitly named as such.
The entire _Components_ folder can be exported as a Library and used in any React application regardless of the presence of a Redux store.
**Layout - The DOM folder**
The _Layout_ folder defines the 'Global' styling and structure of the application. This includes any Navigation, Footers, Menus and the like. These components are purely structural and agnostic to any data that the app intends to process or display.
These components should almost always be [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) or Wrapper Components. These are components that augment or wrap their children (in this case the child is typically the content of the application).
If a layout item needs to be connected to the Redux store, create a connected component that will be wrapped by the parent Layout component. For example
```jsx
//PageContainer.tsx
import {Navbar, AnimatedLogo, PageContainer} from "src/Layout" // Control Page Layout
import {AppMenu, MenuItem} from "menu-library"
import {ConnectedNavbarItem} from "src/Connected/ConnectedNavbarItem" //Navbar item with redux state
...
return (
<Navbar>
<AnimatedLogo>
<ConnectedNavbarItem/>
<AppMenu>
<MenuItem src="/Home"> Home </AppItem>
</AppMenu>
<Navbar>
<PageContainer>
{...props.children}
</PageContainer>
)
```
Application state does not belong inside of the Layout Folder. This folder represents the "canvas" that the rest of the application will be painted onto.
In some applications, the layout folder can be found under the _Components_ directory. This is acceptable, however going forward it will be separated. The intention is to guarantee app state does not find its way to the layout folder.
**I found X in a folder and it doesn't belong there**
This is more likely a mistake than an exception. Consider fixing this in a separate PR. Double check that there isn't a comment justifying its acceptability.
**I think I found an Exception**
Exceptions are rare but may happen. When they do, open a discussion with the team and well document the exception in both the readme and comments in the compone
## 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-optin.azurewebsites.net/#/associate]. The same is true for the front end / Client.
To get to UAT, develop must be merged into the master branch. This will automatically cause the main branch to be built and the resulting release to be pushed to UAT.
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.
## 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)
# 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)
### Bootstrap:
Unified Opt in uses Bootstrap 4
Search the Docs here: https://getbootstrap.com/docs/4.0/getting-started/introduction/
If you see className with "fa fa-noun" we are expecting bootstrap to render that text as an icon. Search all the possible bootstrap fontawesome icons here: https://fontawesome.com/v4.7.0/icons/
### 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.
### 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`
### 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)
# 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,6 @@
# If you don't already have your environment setup using the internal chocolately setup script
# or have chocolately installed on your local machine you will need to perform that step first
choco install mkcert
mkcert -install
mkdir -p .cert
mkcert -key-file ./.cert/key.pem -cert-file ./.cert/cert.pem "localhost"
@@ -0,0 +1,52 @@
parameters:
- name: target_environment
displayName: Target Environment
type: string
default: qa
values:
- qa
- uat
- prd
- name: k8s_namespace
displayName: K8s Namespace to deploy to
type: string
default: landmarkcu
values:
- landmarkcu
variables:
- template: ./config-files/${{ parameters.target_environment }}-pvars.yaml
- name: source_branch_name
value: $[replace(replace(variables['Build.SourceBranch'], 'refs/heads/', ''), '/', '-')]
- name: docker_container_repository_name
value: landmarkcu/lcu.api.internal.incentivemanager-incentivemanager
resources:
repositories:
- repository: azdo_pipeline_templates
type: git
name: Infrastructure Platform/azdo-pipeline-templates
ref: main
stages:
# DOCKER BUILD AND PUSH
- template: docker-build-push.yaml@azdo_pipeline_templates
parameters:
azdo_agent_pool: $(pvar_azdo_agent_pool)
docker_registry_service_connection: $(pvar_docker_registry_service_connection)
docker_container_repository_name: $(docker_container_repository_name)
dockerfile_path: ./Dockerfile
docker_image_tags: |
build-$(Build.BuildId)
$(source_branch_name)
- template: k8s-deploy.yaml@azdo_pipeline_templates
# K8S DEPLOY
parameters:
dependsOn: CI
condition: or(eq(variables['Build.SourceBranchName'], 'develop'), eq(variables['Build.SourceBranchName'], 'main'))
azdo_agent_pool: $(pvar_azdo_agent_pool)
kubectl_version: $(pvar_kubectl_version)
k8s_service_connection: $(pvar_k8s_service_connection)
k8s_namespace: ${{ parameters.k8s_namespace }}
k8s_kustomization_path: ./kustomize/overlays/${{ parameters.target_environment }}
docker_registry_service_connection: $(pvar_docker_registry_service_connection)
docker_image_to_deploy: $(docker_container_repository_name):build-$(Build.BuildId)
@@ -0,0 +1,9 @@
trigger:
branches:
include:
- "*"
exclude:
- "main"
- "develop"
extends:
template: azure-k8s-pipeline.yaml
@@ -0,0 +1,9 @@
trigger:
branches:
include:
- "main"
extends:
template: azure-k8s-pipeline.yaml
parameters:
target_environment: prd
k8s_namespace: landmarkcu
@@ -0,0 +1,9 @@
trigger:
branches:
include:
- "develop"
extends:
template: azure-k8s-pipeline.yaml
parameters:
target_environment: qa
k8s_namespace: landmarkcu
@@ -0,0 +1,9 @@
trigger:
branches:
include:
- "main"
extends:
template: azure-k8s-pipeline.yaml
parameters:
target_environment: uat
k8s_namespace: landmarkcu
@@ -0,0 +1,7 @@
---
variables:
pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-prd-lz-uscz
pvar_docker_registry_service_connection: Docker Registry LMCU
pvar_k8s_service_connection: svcon-sub-df-lcu-apps-prd-aks-k8s-uscz
pvar_kubectl_version: 1.25.9
pvar_azdo_agent_pool_nuget: LCUOnPrem
@@ -0,0 +1,7 @@
---
variables:
pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-qa-lz-uscz
pvar_docker_registry_service_connection: Docker Registry LMCU
pvar_k8s_service_connection: svcon-sub-df-lcu-apps-qa-aks-k8s-uscz
pvar_kubectl_version: 1.25.9
pvar_azdo_agent_pool_nuget: LCUOnPrem
@@ -0,0 +1,7 @@
---
variables:
pvar_azdo_agent_pool: vmss-sub-df-lcu-apps-uat-lz-uscz
pvar_docker_registry_service_connection: Docker Registry LMCU
pvar_k8s_service_connection: svcon-sub-df-lcu-apps-uat-aks-k8s-uscz
pvar_kubectl_version: 1.25.9
pvar_azdo_agent_pool_nuget: LCUOnPrem
@@ -0,0 +1,6 @@
#!/bin/bash
echo "Copy Test Results Out Of Temporary Container";
CONTAINER_ID=`docker images --filter "label=test=true" -q | awk '{ print $1 }'`
docker create --name testcontainer $CONTAINER_ID
docker cp testcontainer:/app/junit ./junit
docker rm testcontainer
@@ -0,0 +1,3 @@
echo "Building Incentive Manager Client Docker Image";
docker build -t incentive-log -f ./Dockerfile .;
@@ -0,0 +1,3 @@
echo "Starting Docker Image For Incentive Manager: will run on port 8080.";
docker run --rm -d -p 8080:80 incentive-log
@@ -0,0 +1,4 @@
echo "[InteractiveMode] Starting Docker Image For Incentive Manager: will run on port 8080.";
echo "Nginx logs will be sent to your command line. Exiting the process desteroys the container."
docker run -it -p 8080:80 incentive-log
@@ -0,0 +1,15 @@
/*eslint-disable*/
/*
Returns Error When Docker Environment Variable is NOT present.
When used with || will SKIP the next step when ENV_IS_DOCKER is true
> node isDockerEnv.js || node version.js
When used with && will stop scripts from running when ENV_IS_DOCKER is missing or false and error out
> node isDockerEnv.js && node test.js
*/
if (process.env.ENV_IS_DOCKER) {
console.log("isDockerEnv: Script is running in Docker.");
process.exit(0);
}
process.exit(1);
@@ -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"
],
}
@@ -0,0 +1,54 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: incentive-manager
spec:
replicas: 1
selector:
matchLabels:
app: incentive-manager
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
labels:
app: incentive-manager
spec:
containers:
- name: incentive-manager
image: incentive-manager
resources:
limits:
memory: "1536Mi"
cpu: "500m"
ports:
- containerPort: 80
livenessProbe:
initialDelaySeconds: 5
periodSeconds: 5
httpGet:
path: /
port: 80
readinessProbe:
initialDelaySeconds: 5
periodSeconds: 5
httpGet:
path: /
port: 80
volumeMounts:
- name: volume-lcu-internal-cert
mountPath: /etc/ssl/certs/lcu-internal-cert.pem
subPath: lcu-internal-cert.pem
- name: volume-appsettings
mountPath: /app/config/mapped-appsettings
volumes:
- name: volume-lcu-internal-cert
configMap:
name: configmap-lcu-internal-cert
- name: volume-appsettings
configMap:
name: incentivemanager-configmap
@@ -0,0 +1,11 @@
---
apiVersion: v1
kind: Service
metadata:
name: incentive-manager
spec:
selector:
app: incentive-manager
ports:
- port: 80
targetPort: 80
@@ -0,0 +1,4 @@
resources:
- Deployment.yaml
- Service.yaml
- virtual-service.yaml
@@ -0,0 +1,11 @@
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: incentive-manager-csi-key-vault-secrets-provider
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "true"
tenantId: 63cd1554-2783-4a86-ae50-b9e6c21c9499
@@ -0,0 +1,18 @@
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: incentive-manager
spec:
gateways:
- istio-system/ingress-gateway
http:
- name: incentive-manager
match:
- uri:
prefix: /
route:
- destination:
host: incentive-manager
port:
number: 80
@@ -0,0 +1,7 @@
resources:
- ../../../base
patchesStrategicMerge:
- set_appsettings_deployment.yaml
images:
- name: incentive-manager
newName: landmarkcu/lcu.api.internal.incentivemanager-incentivemanager
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: incentive-manager
spec:
template:
spec:
containers:
- name: incentive-manager
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Development"
@@ -0,0 +1,2 @@
resources:
- normal
@@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: incentivemanager-configmap
data:
primary.json: |
{
"Primary": {
"SeqUrl": "https://qa-seq.lcu-internal.com",
"CorsAllowedOrigins": [
"http://localhost:3000",
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com"
]
}
}
@@ -0,0 +1,5 @@
resources:
- configmap.yaml
- ../common
patchesStrategicMerge:
- set_virtual_service.yaml
@@ -0,0 +1,18 @@
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: incentive-manager
spec:
hosts:
- qa-incentive-manager.apps.azure.lcu-internal.com
http:
- name: incentive-manager
match:
- uri:
prefix: /
route:
- destination:
host: incentive-manager
port:
number: 80
@@ -0,0 +1,7 @@
resources:
- ../../../base
patchesStrategicMerge:
- set_appsettings_deployment.yaml
images:
- name: incentive-manager
newName: landmarkcu/lcu.api.internal.incentivemanager-incentivemanager
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: incentive-manager
spec:
template:
spec:
containers:
- name: incentive-manager
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
@@ -0,0 +1,2 @@
resources:
- normal
@@ -0,0 +1,16 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: incentivemanager-configmap
data:
primary.json: |
{
"Primary": {
"SeqUrl": "https://seq.lcu-internal.com",
"CorsAllowedOrigins": [
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://api.lcu-internal.com"
]
}
}
@@ -0,0 +1,5 @@
resources:
- configmap.yaml
- ../common
patchesStrategicMerge:
- set_virtual_service.yaml
@@ -0,0 +1,18 @@
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: incentive-manager
spec:
hosts:
- prd-incentive-manager.apps.azure.lcu-internal.com
http:
- name: incentive-manager
match:
- uri:
prefix: /
route:
- destination:
host: incentive-manager
port:
number: 80
@@ -0,0 +1,7 @@
resources:
- ../../../base
patchesStrategicMerge:
- set_appsettings_deployment.yaml
images:
- name: incentive-manager
newName: landmarkcu/lcu.api.internal.incentivemanager-incentivemanager
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: incentive-manager
spec:
template:
spec:
containers:
- name: incentive-manager
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Development"
@@ -0,0 +1,2 @@
resources:
- normal
@@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: incentivemanager-configmap
data:
primary.json: |
{
"Primary": {
"SeqUrl": "https://qa-seq.lcu-internal.com",
"CorsAllowedOrigins": [
"http://localhost:3000",
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://qa-api.lcu-internal.com"
]
}
}
@@ -0,0 +1,5 @@
resources:
- configmap.yaml
- ../common
patchesStrategicMerge:
- set_virtual_service.yaml
@@ -0,0 +1,18 @@
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: incentive-manager
spec:
hosts:
- qa-incentive-manager.apps.azure.lcu-internal.com
http:
- name: incentive-manager
match:
- uri:
prefix: /
route:
- destination:
host: incentive-manager
port:
number: 80
@@ -0,0 +1,7 @@
resources:
- ../../../base
patchesStrategicMerge:
- set_appsettings_deployment.yaml
images:
- name: incentive-manager
newName: landmarkcu/lcu.api.internal.incentivemanager-incentivemanager
@@ -0,0 +1,12 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: incentive-manager
spec:
template:
spec:
containers:
- name: incentive-manager
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Staging"
@@ -0,0 +1,2 @@
resources:
- normal
@@ -0,0 +1,17 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: incentivemanager-configmap
data:
primary.json: |
{
"Primary": {
"SeqUrl": "https://uat-seq.lcu-internal.com",
"CorsAllowedOrigins": [
"http://localhost:3000",
"http://lcu-internal.com",
"https://lcu-internal.com",
"https://uat-api.lcu-internal.com"
]
}
}
@@ -0,0 +1,5 @@
resources:
- configmap.yaml
- ../common
patchesStrategicMerge:
- set_virtual_service.yaml
@@ -0,0 +1,18 @@
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: incentive-manager
spec:
hosts:
- uat-incentive-manager.apps.azure.lcu-internal.com
http:
- name: incentive-manager
match:
- uri:
prefix: /
route:
- destination:
host: incentive-manager
port:
number: 80
@@ -0,0 +1,8 @@
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,191 @@
{
"name": "incentive-manager",
"version": "0.1.4",
"private": true,
"homepage": "/",
"dependencies": {
"@azure/msal-browser": "^2.14.2",
"@azure/msal-react": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@types/node": "^12.19.1",
"@types/react": "^16.9.53",
"@types/react-datepicker": "^3.1.1",
"@types/react-dom": "^16.9.8",
"@types/react-router-dom": "^5.1.6",
"@types/react-toggle": "^4.0.2",
"@types/styled-components": "^5.1.26",
"@types/xlsx": "0.0.36",
"axios": "^0.21.1",
"bootstrap": "^4.5.3",
"connected-react-router": "^6.8.0",
"debounce": "^1.2.1",
"file-saver": "^2.0.5",
"font-awesome": "^4.7.0",
"immutable": "^3.8.1",
"jquery": "^3.5.1",
"lodash.clonedeep": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.throttle": "^4.1.1",
"moment": "^2.29.1",
"popper.js": "^1.16.1",
"react": "^16.14.0",
"react-accessible-accordion": "^3.3.3",
"react-app-polyfill": "^1.0.6",
"react-datepicker": "^2.9.6",
"react-dom": "^16.14.0",
"react-multi-select-component": "2.0.14",
"react-redux": "^7.2.1",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.3",
"react-select": "^5.2.1",
"react-toastify": "^6.0.9",
"react-toggle": "^4.1.1",
"reactstrap": "^8.9.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"seamless-immutable": "^7.1.3",
"typescript": "^4.0.1",
"web-vitals": "^0.2.4",
"xlsx": "^0.17.4"
},
"scripts": {
"start": "react-scripts start",
"start-https": "set HTTPS=true&&set SSL_CRT_FILE=.cert/cert.pem&&set SSL_KEY_FILE=.cert/key.pem&&react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test-ci": "react-scripts test --watchAll=false --reporters=default --reporters=jest-junit",
"eject": "react-scripts eject",
"postinstall": "node isDockerEnv.js || 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 --reporters ./node_modules/jest-junit",
"refreshVSToken": "vsts-npm-auth -config .npmrc"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"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",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@types/debounce": "^1.2.0",
"@types/enzyme": "^3.9.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/faker": "^4.1.8",
"@types/file-saver": "^2.0.2",
"@types/jest": "^26.0.15",
"@types/lodash.clonedeep": "^4.5.6",
"@types/lodash.pick": "^4.4.6",
"@types/lodash.throttle": "^4.1.6",
"@types/moxios": "^0.4.8",
"@types/react-modal": "^3.10.6",
"@types/react-redux": "^7.0.9",
"@types/redux-thunk": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^4.5.0",
"@typescript-eslint/parser": "^4.5.0",
"commitizen": "^4.2.2",
"cz-conventional-changelog": "^2.1.0",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.5",
"eslint": "^7.12.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.14.0",
"eslint-config-react": "^1.1.7",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-jsx-a11y": "^6.3.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.0",
"eslint-plugin-react-hooks": "^4.0.0",
"git-describe": "^4.0.4",
"husky": "^2.7.0",
"jest": "^26.6.0",
"jest-junit": "^10.0.0",
"node-sass": "^4.14.1",
"prettier": "^2.1.2",
"pretty-quick": "^3.1.0",
"redux-devtools-extension": "^2.13.8",
"standard-version": "^9.0.0",
"ts-jest": "^26.4.2",
"validate-commit-msg": "^2.14.0",
"vsts-npm-auth": "^0.37.0"
},
"husky": {
"hooks": {
"applypatch-msg": "echo \"[Husky] applypatch-msg\"",
"pre-applypatch": "echo \"[Husky] pre-applypatch\"",
"post-applypatch": "echo \"[Husky] post-applypatch\"",
"pre-commit": "pretty-quick --staged",
"pre-merge-commit": "echo \"[Husky] pre-merge-commit\"",
"prepare-commit-msg": "echo \"[Husky] prepare-commit-msg\"",
"commit-msg": "echo \"[Husky] commit-msg\"",
"post-commit": "echo \"[Husky] post-commit\"",
"pre-rebase": "echo \"[Husky] pre-rebase\"",
"post-checkout": "echo \"[Husky] post-checkout\"",
"post-merge": "echo \"[Husky] post-merge\"",
"pre-push": "echo \"[Husky] pre-push\"",
"pre-receive": "echo \"[Husky] pre-receive\"",
"update": "echo \"[Husky] update\"",
"post-receive": "echo \"[Husky] post-receive\"",
"post-update": "echo \"[Husky] post-update\"",
"reference-transaction": "echo \"[Husky] reference-transaction\"",
"push-to-checkout": "echo \"[Husky] push-to-checkout\"",
"pre-auto-gc": "echo \"[Husky] pre-auto-gc\"",
"post-rewrite": "echo \"[Husky] post-rewrite\"",
"sendemail-validate": "echo \"[Husky] sendemail-validate\"",
"fsmonitor-watchman": "echo \"[Husky] fsmonitor-watchman\"",
"p4-changelist": "echo \"[Husky] p4-changelist\"",
"p4-prepare-changelist": "echo \"[Husky] p4-prepare-changelist\"",
"p4-post-changelist": "echo \"[Husky] p4-post-changelist\"",
"p4-pre-submit": "echo \"[Husky] p4-pre-submit\"",
"post-index-change": "echo \"[Husky] post-index-change\""
}
},
"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": ""
}
},
"jest-junit": {
"suiteName:": "jest tests",
"outputDirectory": "./junit",
"outputName": "junit.xml"
}
}
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="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Landmark Credit Union Incentive Manager"
/>
<link rel="icon" href="/favicon.ico" />
<!--
manifest.json provides metadata used when your web app is installed on a
appUser's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="/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>Incentive 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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -0,0 +1,26 @@
{
"short_name": "Incentive Manager",
"name": "Incentive Manager",
"description": "Manage and view Landmark Incentives",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
@@ -0,0 +1,7 @@
import React from "react";
describe("Fake Test", () => {
it("Does Not Work", () => {
expect(true).toBe(true);
});
});
@@ -0,0 +1,293 @@
import React, { useEffect } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import NotFound404Page from "./components/status-pages/NotFound404Page";
import TestPage from "./components/status-pages/TestPage";
import Configuration from "./shared/configuration/Configuration";
import AppContainer from "./components/layout/AppContainer";
import ActivityPage from "./containers/activity-page/ActivityPage";
import ActivityDetailPage from "./containers/activity-detail-page/ActivityDetailPage";
import DepartmentGoalsPage from "./containers/goals/department/DepartmentGoalsPage";
import PersonnelGoalsPage from "./containers/goals/personnel/PersonnelGoalsPage";
import OrganizationView from "./containers/organization-view/OrganizationView";
import { useDispatch } from "react-redux";
import RoleManagement from "./containers/role-management/RoleMangement";
import PayoutProfilePage from "./containers/payout-profile/PayoutProfilePage";
import CommissionsPage from "./containers/commissions-page/CommissionsPage";
import ProfileSearchPage from "./containers/profile/ProfileSearchPage";
import ProfileSelector from "./containers/profile/ProfileSelector";
import { refreshProfile } from "./actions/ProfileActions";
import ExportPayoutPage from "./containers/payouts/ExportPayoutPage";
import DashboardPage from "./containers/dashboard/DashboardPage";
import ActivityFormPage from "./containers/activity-form-page/ActivityFormPage";
import { SettingsProvider } from "./shared/providers/SettingsProvider";
import DepartmentDetailPage from "./containers/department-detail-page/DepartmentDetailPage";
import PermissionGuard from "./containers/shared/PermissionGuard";
import EndOfEmploymentPage from "./containers/eoe-list/EndOfEmploymentPage";
import DeadActivityPage from "./containers/dead-activity-page/DeadActivityPage";
import RoleMappingPage from "./containers/role-mapping-page/RoleMappingPage";
import ReportPage from "./containers/report-page/ReportPage";
const App: React.FC = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(refreshProfile());
}, []);
return (
//Normally we use Connected Router which requires has. React-Dom router does not require a hash.
<AppContainer>
<SettingsProvider>
<BrowserRouter basename={Configuration.Get.appUrlBaseName}>
<Switch>
<Route
exact
path={Configuration.Get.appUrls.root}
render={() => <DashboardPage />}
/>
<Route
exact
path={Configuration.Get.appUrls.dashboard}
render={() => <DashboardPage />}
/>
<Route
exact
path={Configuration.Get.appUrls.activityForm}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"CREATE_ACTIVITY"
]}
>
<ActivityFormPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.activity}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"VIEW_ACTIVITY_AUDIT"
]}
>
<ActivityPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={`${Configuration.Get.appUrls.activity}/:id`}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"VIEW_ACTIVITY_AUDIT"
]}
>
<ActivityDetailPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.deadActivity}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"VIEW_DEAD_ACTIVITY_QUEUE"
]}
>
<DeadActivityPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={`${Configuration.Get.appUrls.department}`}
render={() => <DepartmentDetailPage />}
/>
<Route
exact
path={Configuration.Get.appUrls.exportPayout}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"GENERATE_PAYOUT_FILES"
]}
>
<ExportPayoutPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.eoeList}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"GENERATE_PAYOUT_FILES"
]}
>
<EndOfEmploymentPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.departmentGoals}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"MANAGE_DEPARTMENT_GOALS"
]}
>
<DepartmentGoalsPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.personnelGoals}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"MANAGE_PERSONNEL_GOALS"
]}
>
<PersonnelGoalsPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.commissions}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"MANAGE_DEPARTMENT_GOALS"
]}
>
<CommissionsPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.profile}
render={() => <ProfileSelector />}
/>
<Route
exact
path={Configuration.Get.appUrls.payoutProfiles}
render={() => <PayoutProfilePage />}
/>
<Route
exact
path={Configuration.Get.appUrls.profileSearch}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"MANAGE_DEPARTMENT_GOALS",
"MANAGE_PERSONNEL_GOALS",
"ASSIGN_ASSOCIATES_TO_DEPARTMENT"
]}
>
<ProfileSearchPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.testRoute}
render={(routerProps) => (
<TestPage {...routerProps} />
)}
/>
<Route
exact
path={Configuration.Get.appUrls.organization}
render={(routerProps) => (
<OrganizationView {...routerProps} />
)}
/>
<Route
exact
path={Configuration.Get.appUrls.roles}
render={(routerProps) => (
<RoleManagement {...routerProps} />
)}
/>
<Route
exact
path={Configuration.Get.appUrls.roleMappings}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={[
"MANAGE_ROLE_MAPPINGS"
]}
>
<RoleMappingPage />
</PermissionGuard>
);
}}
/>
<Route
exact
path={Configuration.Get.appUrls.reports}
render={() => {
return (
<PermissionGuard
showWarning
requiredPermissions={["VIEW_REPORTS"]}
>
<ReportPage />
</PermissionGuard>
);
}}
/>
<Route
path="*"
render={(routerProps) => (
<NotFound404Page {...routerProps} />
)}
/>
</Switch>
</BrowserRouter>
</SettingsProvider>
</AppContainer>
);
};
export default App;
@@ -0,0 +1,172 @@
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import {
IActivitySearchForm,
IAuditableActivity,
IDepartmentProgress,
IEmployeeProgress
} from "../shared/models/Activity";
import ActivityService from "./../shared/services/ActivityService";
import { IAppState } from "../store/Store";
import IPagedResponse from "./../shared/types/IPagedResponse";
import { IMonthRange } from "../shared/types/IMonthRange";
export enum ActivityActionTypes {
GET_ACTIVITIES = "ACTIVITY_GET_ACTIVITIES",
GET_NEXT_ACTIVITY_PAGE = "GET_NEXT_ACTIVITY_PAGE",
GET_LAST_ACTIVITY_PAGE = "GET_LAST_ACTIVITY_PAGE",
GET_MY_ACTIVITIES = "ACTIVITY_GET_MY_ACTIVITIES",
GET_MY_DEPARTMENT_ACTIVITIES = "ACTIVITY_GET_MY_DEPARTMENT_ACTIVITIES",
SET_ACTIVITY_STATES_FILTER = "SET_ACTIVITY_STATES_FILTER"
}
export interface IActivityGetAllAction {
type: ActivityActionTypes.GET_ACTIVITIES;
activities: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
}
export interface IActivityGetNextPageAction {
type: ActivityActionTypes.GET_NEXT_ACTIVITY_PAGE;
activities: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
}
export interface IActivityGetLastPageAction {
type: ActivityActionTypes.GET_LAST_ACTIVITY_PAGE;
activities: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
}
export interface IActivitySetStateFilter {
type: ActivityActionTypes.SET_ACTIVITY_STATES_FILTER;
allowedStates: string[];
}
export interface IActivityGetMyAction {
type: ActivityActionTypes.GET_MY_ACTIVITIES;
progress: IAsyncStateContainer<IEmployeeProgress>;
}
export interface IActivityGetMyDepartmentAction {
type: ActivityActionTypes.GET_MY_DEPARTMENT_ACTIVITIES;
departmentProgress: IAsyncStateContainer<IDepartmentProgress[]>;
}
export type ActivityActions =
| IActivityGetAllAction
| IActivityGetMyAction
| IActivityGetMyDepartmentAction
| IActivityGetNextPageAction
| IActivityGetLastPageAction
| IActivitySetStateFilter;
export const getAllActivities: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IActivityGetAllAction>
> = (page: number, limit: number, filters: IActivitySearchForm) => {
return async (dispatch: Dispatch<ActivityActions>) => {
dispatch({
type: ActivityActionTypes.GET_ACTIVITIES,
activities: { state: AsyncState.Loading }
});
const newActivities = await ActivityService.getPaginatedActivities(
page,
limit,
filters
);
dispatch({
type: ActivityActionTypes.GET_ACTIVITIES,
activities: newActivities
});
};
};
export const getNextPageOfActivities: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IActivityGetNextPageAction>
> = () => {
return async (dispatch: Dispatch<ActivityActions>, getState) => {
const activityPageResponse = getState().activityState
.filteredActivities;
if (activityPageResponse?.data?.nextPage) {
dispatch({
type: ActivityActionTypes.GET_NEXT_ACTIVITY_PAGE,
activities: { state: AsyncState.Loading }
});
const newActivities = await ActivityService.getPaginatedActivitiesFromUrl(
activityPageResponse.data.nextPage
// you don't need filters...
);
dispatch({
type: ActivityActionTypes.GET_NEXT_ACTIVITY_PAGE,
activities: newActivities
});
}
};
};
export const getLastPageOfActivities: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IActivityGetLastPageAction>
> = () => {
return async (dispatch: Dispatch<ActivityActions>, getState) => {
const activityPageResponse = getState().activityState
.filteredActivities;
if (activityPageResponse?.data?.previousPage) {
dispatch({
type: ActivityActionTypes.GET_LAST_ACTIVITY_PAGE,
activities: { state: AsyncState.Loading }
});
const newActivities = await ActivityService.getPaginatedActivitiesFromUrl(
activityPageResponse.data.previousPage
// you don't need the filters, they're in the page url...
);
dispatch({
type: ActivityActionTypes.GET_LAST_ACTIVITY_PAGE,
activities: newActivities
});
}
};
};
export const getMyProgress: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IActivityGetMyAction>
> = (monthRange: IMonthRange) => {
return async (dispatch: Dispatch<IActivityGetMyAction>) => {
dispatch({
type: ActivityActionTypes.GET_MY_ACTIVITIES,
progress: { state: AsyncState.Loading }
});
const newActivities = await ActivityService.getMyProgress(monthRange);
dispatch({
type: ActivityActionTypes.GET_MY_ACTIVITIES,
progress: newActivities
});
};
};
export const getMyDepartmentProgress: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IActivityGetMyDepartmentAction>
> = (monthRange: IMonthRange) => {
return async (dispatch: Dispatch<IActivityGetMyDepartmentAction>) => {
dispatch({
type: ActivityActionTypes.GET_MY_DEPARTMENT_ACTIVITIES,
departmentProgress: { state: AsyncState.Loading }
});
const departmentActivities = await ActivityService.getMyDepartmentProgress(
monthRange
);
dispatch({
type: ActivityActionTypes.GET_MY_DEPARTMENT_ACTIVITIES,
departmentProgress: departmentActivities
});
};
};
@@ -0,0 +1,43 @@
import { Dispatch } from "react";
import { ActionCreator } from "redux";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import { IAuditableCommission } from "../shared/models/Commission";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IAppState } from "../store/Store";
import CommissionService from "../shared/services/CommissionService";
export enum CommissionActionTypes {
GET_ALL_COMMISSIONS = "GET_ALL_COMMISSIONS",
SET_COMMISSIONS = "SET_COMMISSIONS"
}
export interface ICommissionsGetAllAction {
type: CommissionActionTypes.GET_ALL_COMMISSIONS;
commissions: IAsyncStateContainer<IAuditableCommission[]>;
}
export interface ICommissionsSetAction {
type: CommissionActionTypes.SET_COMMISSIONS;
submitState: IAsyncStateContainer<string>;
}
export type CommissionActions = ICommissionsGetAllAction;
export const getAllCommissions: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, ICommissionsGetAllAction>
> = () => {
return async (dispatch: Dispatch<ICommissionsGetAllAction>) => {
dispatch({
type: CommissionActionTypes.GET_ALL_COMMISSIONS,
commissions: { state: AsyncState.Loading }
});
const allCommissions = await CommissionService.getAllCommissions();
dispatch({
type: CommissionActionTypes.GET_ALL_COMMISSIONS,
commissions: allCommissions
});
};
};
@@ -0,0 +1,125 @@
import { Dispatch } from "react";
import { ActionCreator } from "redux";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import {
IAuditableDeadActivity,
IDeadActivitySearchForm
} from "../shared/models/DeadActivity";
import DeadActivityService from "../shared/services/DeadActivityService";
import DeadDeadActivityService from "../shared/services/DeadActivityService";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import IPagedResponse from "../shared/types/IPagedResponse";
import { IAppState } from "../store/Store";
export enum DeadActivityActionTypes {
GET_DEAD_ACTIVITIES = "DEAD_ACTIVITY_GET_DEAD_ACTIVITIES",
GET_NEXT_DEAD_ACTIVITY_PAGE = "GET_NEXT_DEAD_ACTIVITY_PAGE",
GET_LAST_DEAD_ACTIVITY_PAGE = "GET_LAST_DEAD_ACTIVITY_PAGE",
SET_DEAD_ACTIVTY_STATE_FILTER = "SET_DEAD_ACTIVITY_STATE_FILTER"
}
export interface IDeadActivityGetAllAction {
type: DeadActivityActionTypes.GET_DEAD_ACTIVITIES;
deadActivities: IAsyncStateContainer<
IPagedResponse<IAuditableDeadActivity>
>;
}
export interface IDeadActivityGetNextPageAction {
type: DeadActivityActionTypes.GET_NEXT_DEAD_ACTIVITY_PAGE;
deadActivities: IAsyncStateContainer<
IPagedResponse<IAuditableDeadActivity>
>;
}
export interface IDeadActivityGetLastPageAction {
type: DeadActivityActionTypes.GET_LAST_DEAD_ACTIVITY_PAGE;
deadActivities: IAsyncStateContainer<
IPagedResponse<IAuditableDeadActivity>
>;
}
export interface IDeadActivityStateFilter {
type: DeadActivityActionTypes.SET_DEAD_ACTIVTY_STATE_FILTER;
allowedStates: string[];
}
export type DeadActivityActions =
| IDeadActivityGetAllAction
| IDeadActivityGetNextPageAction
| IDeadActivityGetLastPageAction
| IDeadActivityStateFilter;
export const getAllDeadActivities: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IDeadActivityGetAllAction>
> = (page: number, limit: number, filters: IDeadActivitySearchForm) => {
return async (dispatch: Dispatch<DeadActivityActions>) => {
dispatch({
type: DeadActivityActionTypes.GET_DEAD_ACTIVITIES,
deadActivities: { state: AsyncState.Loading }
});
const newDeadActivities = await DeadDeadActivityService.getPaginatedDeadActivities(
page,
limit,
filters
);
dispatch({
type: DeadActivityActionTypes.GET_DEAD_ACTIVITIES,
deadActivities: newDeadActivities
});
};
};
export const getNextPageOfDeadActivities: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IDeadActivityGetNextPageAction>
> = () => {
return async (dispatch: Dispatch<DeadActivityActions>, getState) => {
const deadActivityPageResponse = getState().deadActivityState
.filteredDeadActivities;
if (deadActivityPageResponse?.data?.nextPage) {
dispatch({
type: DeadActivityActionTypes.GET_NEXT_DEAD_ACTIVITY_PAGE,
deadActivities: { state: AsyncState.Loading }
});
const newDeadActivities = await DeadActivityService.getPaginatedDeadActivitiesFromUrl(
deadActivityPageResponse.data.nextPage
// you don't need filters...
);
dispatch({
type: DeadActivityActionTypes.GET_NEXT_DEAD_ACTIVITY_PAGE,
deadActivities: newDeadActivities
});
}
};
};
export const getLastPageOfDeadActivities: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IDeadActivityGetLastPageAction>
> = () => {
return async (dispatch: Dispatch<DeadActivityActions>, getState) => {
const deadActivityPageResponse = getState().deadActivityState
.filteredDeadActivities;
if (deadActivityPageResponse?.data?.previousPage) {
dispatch({
type: DeadActivityActionTypes.GET_LAST_DEAD_ACTIVITY_PAGE,
deadActivities: { state: AsyncState.Loading }
});
const newDeadActivities = await DeadActivityService.getPaginatedDeadActivitiesFromUrl(
deadActivityPageResponse.data.previousPage
// you don't need the filters, they're in the page url...
);
dispatch({
type: DeadActivityActionTypes.GET_LAST_DEAD_ACTIVITY_PAGE,
deadActivities: newDeadActivities
});
}
};
};
@@ -0,0 +1,91 @@
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IDepartment, IDepartmentDto } from "../shared/models/Department";
import { ActionCreator, Dispatch } from "redux";
import { IAppState } from "../store/Store";
import DepartmentService from "./../shared/services/DepartmentService";
import AsyncState from "../shared/enumerations/AsyncState";
import { ThunkAction } from "redux-thunk";
export enum DepartmentActionTypes {
GET_ALL_DEPARTMENTS = "GET_ALL_DEPARTMENTS",
ADD_NEW_DEPARTMENT = "ADD_NEW_DEPARTNEMTN",
UPDATE_EXISTING_DEPARTNEMTN = "UPDATE_EXISTING_DEPARTMENT"
}
export interface IDepartmentGetAllAction {
type: DepartmentActionTypes.GET_ALL_DEPARTMENTS;
departments: IAsyncStateContainer<IDepartment[]>;
}
export interface IDepartmentsAddNew {
type: DepartmentActionTypes.ADD_NEW_DEPARTMENT;
newDepartment: IAsyncStateContainer<IDepartment>;
}
export interface IDepartmentUpdateExisting {
type: DepartmentActionTypes.UPDATE_EXISTING_DEPARTNEMTN;
updatedDepartment: IAsyncStateContainer<IDepartment>;
}
export type DepartmentActions =
| IDepartmentGetAllAction
| IDepartmentsAddNew
| IDepartmentUpdateExisting;
export const getAllDepartments: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IDepartmentGetAllAction>
> = () => {
return async (dispatch: Dispatch<IDepartmentGetAllAction>) => {
dispatch({
type: DepartmentActionTypes.GET_ALL_DEPARTMENTS,
departments: { state: AsyncState.Loading }
});
const newDepartments = await DepartmentService.getAllDepartments();
dispatch({
type: DepartmentActionTypes.GET_ALL_DEPARTMENTS,
departments: newDepartments
});
};
};
export const submitNewDepartment: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IDepartmentsAddNew>
> = (newDepartment: IDepartmentDto) => {
return async (dispatch: Dispatch<IDepartmentsAddNew>) => {
dispatch({
type: DepartmentActionTypes.ADD_NEW_DEPARTMENT,
newDepartment: { state: AsyncState.Loading }
});
const submittedDepartment = await DepartmentService.submitNewDepartment(
newDepartment
);
dispatch({
type: DepartmentActionTypes.ADD_NEW_DEPARTMENT,
newDepartment: submittedDepartment
});
};
};
export const updateExistingDepartment: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IDepartmentGetAllAction>
> = (id: number, newDepartment: IDepartmentDto) => {
return async (dispatch: Dispatch<IDepartmentUpdateExisting>) => {
dispatch({
type: DepartmentActionTypes.UPDATE_EXISTING_DEPARTNEMTN,
updatedDepartment: { state: AsyncState.Loading }
});
const submittedDepartment = await DepartmentService.updateExistingDepartment(
id,
newDepartment
);
dispatch({
type: DepartmentActionTypes.UPDATE_EXISTING_DEPARTNEMTN,
updatedDepartment: submittedDepartment
});
};
};
@@ -0,0 +1,113 @@
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IEmployee } from "../shared/models/Employee";
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IAppState } from "../store/Store";
import AsyncState from "../shared/enumerations/AsyncState";
import EmployeeService from "../shared/services/EmployeeService";
import {
IDepartmentAssignmentDto,
IDepartmentAssignmentUpdateDto
} from "../shared/models/DepartmentAssignment";
export enum EmployeeActionTypes {
SELECT_EMPLOYEE = "SELECT_EMPLOYEE",
SUBMIT_DEPT_ASSIGNMENT = "SUBMIT_DEPARTMENT_ASSIGNMENT",
SUBMIT_END_DEPARTMENT_ASSIGNMENT = "SUBMIT_END_DEPARTMENT_ASSIGNMENT"
}
export interface IEmployeeSelectAction {
type: EmployeeActionTypes.SELECT_EMPLOYEE;
selectedEmployee: IAsyncStateContainer<IEmployee>;
}
export interface IEmployeeSubmitDeptAssignmentAction {
type: EmployeeActionTypes.SUBMIT_DEPT_ASSIGNMENT;
submitState: IAsyncStateContainer<string>;
}
export interface IEmployeeSubmitEndDeptAssignmentAction {
type: EmployeeActionTypes.SUBMIT_END_DEPARTMENT_ASSIGNMENT;
submitState: IAsyncStateContainer<string>;
}
export type EmployeeActions =
| IEmployeeSelectAction
| IEmployeeSubmitDeptAssignmentAction
| IEmployeeSubmitEndDeptAssignmentAction;
export const trySelectEmployee: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IEmployeeSelectAction>
> = (employeeId: string) => {
return async (dispatch: Dispatch<IEmployeeSelectAction>) => {
dispatch({
type: EmployeeActionTypes.SELECT_EMPLOYEE,
selectedEmployee: { state: AsyncState.Loading }
});
const retreivedEmployee = await EmployeeService.getByEmployeeId(
employeeId
);
dispatch({
type: EmployeeActionTypes.SELECT_EMPLOYEE,
selectedEmployee: retreivedEmployee
});
};
};
export const submitDepartmentAssignment: ActionCreator<
ThunkAction<
Promise<any>,
IAppState,
null,
IEmployeeSubmitDeptAssignmentAction
>
> = (
employeeId: string,
deptAssignmentData: Partial<IDepartmentAssignmentDto>
) => {
return async (dispatch: Dispatch<IEmployeeSubmitDeptAssignmentAction>) => {
dispatch({
type: EmployeeActionTypes.SUBMIT_DEPT_ASSIGNMENT,
submitState: { state: AsyncState.Loading }
});
const postState = await EmployeeService.assignNewDepartment(
deptAssignmentData,
employeeId
);
dispatch({
type: EmployeeActionTypes.SUBMIT_DEPT_ASSIGNMENT,
submitState: postState
});
};
};
export const submitDepartmentAssignmentUpdate: ActionCreator<
ThunkAction<
Promise<any>,
IAppState,
null,
IEmployeeSubmitEndDeptAssignmentAction
>
> = (assignment: IDepartmentAssignmentUpdateDto) => {
return async (
dispatch: Dispatch<IEmployeeSubmitEndDeptAssignmentAction>
) => {
dispatch({
type: EmployeeActionTypes.SUBMIT_END_DEPARTMENT_ASSIGNMENT,
submitState: { state: AsyncState.Loading }
});
const postState = await EmployeeService.updateDepartmentAssignment(
assignment
);
dispatch({
type: EmployeeActionTypes.SUBMIT_END_DEPARTMENT_ASSIGNMENT,
submitState: postState
});
};
};
@@ -0,0 +1,153 @@
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IMonthRange } from "../shared/types/IMonthRange";
import { IGoalState } from "../reducers/GoalReducer";
import AsyncState from "../shared/enumerations/AsyncState";
import {
IDepartmentGoals,
IDepartmentGoalsForRange,
IDepartmentGoalsInput,
IEmployeeGoals,
IGoal,
IPersonnelGoals,
IPersonnelGoalsInput
} from "../shared/models/Goal";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import GoalService from "./../shared/services/GoalService";
export enum GoalActionTypes {
GET_ALL_GOALS = "GET_ALL_GOALS",
GET_DEPT_GOALS = "GET_DEPT_GOALS",
GET_PERS_GOALS = "GET_PERS_GOALS",
GET_MY_GOALS = "GET_MY_GOALS",
GET_MY_DEPTS_GOALS = "GET_MY_DEPTS_GOALS"
}
export interface IGoalGetAllAction {
type: GoalActionTypes.GET_ALL_GOALS;
retrievedGoals: IAsyncStateContainer<IGoal[]>;
}
export interface IGoalGetMyAction {
type: GoalActionTypes.GET_MY_GOALS;
currentUserGoals: IAsyncStateContainer<IEmployeeGoals>;
}
export interface IGoalGetMyDeptsAction {
type: GoalActionTypes.GET_MY_DEPTS_GOALS;
currentUsersDepartmentsGoals: IAsyncStateContainer<
IDepartmentGoalsForRange[]
>;
}
export interface IGoalGetDeptAction {
type: GoalActionTypes.GET_DEPT_GOALS;
retrievedGoals: IAsyncStateContainer<IDepartmentGoals>;
}
export interface IGoalGetPersAction {
type: GoalActionTypes.GET_PERS_GOALS;
retrievedGoals: IAsyncStateContainer<IPersonnelGoals>;
}
export type GoalActions =
| IGoalGetMyAction
| IGoalGetMyDeptsAction
| IGoalGetAllAction
| IGoalGetDeptAction
| IGoalGetPersAction;
export const queryGoals: ActionCreator<
ThunkAction<Promise<any>, IGoalState, null, IGoalGetAllAction>
> = (range: IMonthRange, includePersonnel: boolean, departmentId?: number) => {
return async (dispatch: Dispatch<IGoalGetAllAction>) => {
dispatch({
type: GoalActionTypes.GET_ALL_GOALS,
retrievedGoals: { state: AsyncState.Loading }
});
const retrievedGoals = await GoalService.getAllGoals(
range,
includePersonnel,
departmentId
);
dispatch({
type: GoalActionTypes.GET_ALL_GOALS,
retrievedGoals: retrievedGoals
});
};
};
export const getMyGoals: ActionCreator<
ThunkAction<Promise<any>, IGoalState, null, IGoalGetMyAction>
> = (monthRange: IMonthRange) => {
return async (dispatch: Dispatch<IGoalGetMyAction>) => {
dispatch({
type: GoalActionTypes.GET_MY_GOALS,
currentUserGoals: { state: AsyncState.Loading }
});
const retrievedGoals = await GoalService.getMyGoals(monthRange);
dispatch({
type: GoalActionTypes.GET_MY_GOALS,
currentUserGoals: retrievedGoals
});
};
};
export const getMyDepartmentsGoals: ActionCreator<
ThunkAction<Promise<any>, IGoalState, null, IGoalGetMyDeptsAction>
> = (monthRange: IMonthRange) => {
return async (dispatch: Dispatch<IGoalGetMyDeptsAction>) => {
dispatch({
type: GoalActionTypes.GET_MY_DEPTS_GOALS,
currentUsersDepartmentsGoals: { state: AsyncState.Loading }
});
const retrievedGoals = await GoalService.getMyDepartmentsGoals(
monthRange
);
dispatch({
type: GoalActionTypes.GET_MY_DEPTS_GOALS,
currentUsersDepartmentsGoals: retrievedGoals
});
};
};
export const getDepartmentGoals: ActionCreator<
ThunkAction<Promise<any>, IGoalState, null, IGoalGetDeptAction>
> = (input: IDepartmentGoalsInput) => {
return async (dispatch: Dispatch<IGoalGetDeptAction>) => {
dispatch({
type: GoalActionTypes.GET_DEPT_GOALS,
retrievedGoals: { state: AsyncState.Loading }
});
const retrievedGoals = await GoalService.getDepartmentGoals(input);
dispatch({
type: GoalActionTypes.GET_DEPT_GOALS,
retrievedGoals: retrievedGoals
});
};
};
export const getPersonnelGoals: ActionCreator<
ThunkAction<Promise<any>, IGoalState, null, IGoalGetPersAction>
> = (input: IPersonnelGoalsInput) => {
return async (dispatch: Dispatch<IGoalGetPersAction>) => {
dispatch({
type: GoalActionTypes.GET_PERS_GOALS,
retrievedGoals: { state: AsyncState.Loading }
});
const retrievedGoals = await GoalService.getPersonnelGoals(input);
dispatch({
type: GoalActionTypes.GET_PERS_GOALS,
retrievedGoals: retrievedGoals
});
};
};
@@ -0,0 +1,65 @@
import { ActionCreator, Dispatch } from "redux";
import { ThunkAction } from "redux-thunk";
import { IGoalState } from "../reducers/GoalReducer";
import AsyncState from "../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IPayoutState } from "../reducers/PayoutReducer";
import PayoutService from "../shared/services/PayoutService";
import {
IEmployeePayoutsInput,
IPayoutLineItem
} from "../shared/models/Payout";
import { IMonthRange } from "../shared/types/IMonthRange";
export enum PayoutActionTypes {
GET_MY_PAYOUTS = "GET_MY_PAYOUTS",
GET_EMP_PAYOUTS = "GET_EMP_PAYOUTS"
}
export interface IPayoutsGetMyAction {
type: PayoutActionTypes.GET_MY_PAYOUTS;
currentUserPayouts: IAsyncStateContainer<IPayoutLineItem[]>;
}
export interface IPayoutsGetEmpAction {
type: PayoutActionTypes.GET_EMP_PAYOUTS;
retrievedPayouts: IAsyncStateContainer<IPayoutLineItem[]>;
}
export type PayoutActions = IPayoutsGetMyAction | IPayoutsGetEmpAction;
export const getMyPayouts: ActionCreator<
ThunkAction<Promise<any>, IPayoutState, null, IPayoutsGetMyAction>
> = (month: number, year: number) => {
return async (dispatch: Dispatch<IPayoutsGetMyAction>) => {
dispatch({
type: PayoutActionTypes.GET_MY_PAYOUTS,
currentUserPayouts: { state: AsyncState.Loading }
});
const retrievedPayouts = await PayoutService.getMyPayouts(month, year);
dispatch({
type: PayoutActionTypes.GET_MY_PAYOUTS,
currentUserPayouts: retrievedPayouts
});
};
};
export const getEmployeePayouts: ActionCreator<
ThunkAction<Promise<any>, IGoalState, null, IPayoutsGetEmpAction>
> = (input: IEmployeePayoutsInput) => {
return async (dispatch: Dispatch<IPayoutsGetEmpAction>) => {
dispatch({
type: PayoutActionTypes.GET_EMP_PAYOUTS,
retrievedPayouts: { state: AsyncState.Loading }
});
const retrievedPayouts = await PayoutService.getEmployeePayouts(input);
dispatch({
type: PayoutActionTypes.GET_EMP_PAYOUTS,
retrievedPayouts: retrievedPayouts
});
};
};
@@ -0,0 +1,63 @@
import { ActionCreator, Dispatch } from "redux";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IAppState } from "./../store/Store";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import { IEmployee, IEmployeeSummary } from "../shared/models/Employee";
import ProfileDao from "../shared/dao/ProfileDao";
export enum ProfileActionTypes {
REFRESH_PROFILE = "REFRESH_PROFILE",
SEARCH_PROFILE = "SEARCH_PROFILE"
}
export interface IProfileRefreshAction {
type: ProfileActionTypes.REFRESH_PROFILE;
profile: IAsyncStateContainer<IEmployee>;
}
export interface IProfileSearchAction {
type: ProfileActionTypes.SEARCH_PROFILE;
searchResults: IAsyncStateContainer<IEmployeeSummary[]>;
}
export type ProfileActions = IProfileRefreshAction | IProfileSearchAction;
export const refreshProfile: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IProfileRefreshAction>
> = (employeeId?: string) => {
return async (dispatch: Dispatch<IProfileRefreshAction>) => {
dispatch({
type: ProfileActionTypes.REFRESH_PROFILE,
profile: { state: AsyncState.Loading }
});
const retrievedProfile = await ProfileDao.getProfile(employeeId);
dispatch({
type: ProfileActionTypes.REFRESH_PROFILE,
profile: retrievedProfile
});
};
};
export const profileSearch: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IProfileSearchAction>
> = (searchQuery: string, role?: string) => {
return async (dispatch: Dispatch<IProfileSearchAction>) => {
dispatch({
type: ProfileActionTypes.SEARCH_PROFILE,
searchResults: { state: AsyncState.Loading }
});
const retrievedProfile = await ProfileDao.profileSearch(
searchQuery,
role
);
dispatch({
type: ProfileActionTypes.SEARCH_PROFILE,
searchResults: retrievedProfile
});
};
};
@@ -0,0 +1,72 @@
import { Dispatch } from "react";
import { ActionCreator } from "redux";
import { ThunkAction } from "redux-thunk";
import AsyncState from "../shared/enumerations/AsyncState";
import {
IAuditableRoleMapping,
IRoleMappingChange
} from "../shared/models/RoleMapping";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import { IAppState } from "../store/Store";
import RoleMappingService from "../shared/services/RoleMappingService";
export enum RoleMappingActionTypes {
GET_ALL_ROLE_MAPPINGS = "GET_ALL_ROLE_MAPPINGS",
SET_ROLE_MAPPINGS = "SET_ROLE_MAPPINGS",
SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE = "SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE"
}
export interface IRoleMappingsGetAllAction {
type: RoleMappingActionTypes.GET_ALL_ROLE_MAPPINGS;
roleMappings: IAsyncStateContainer<IAuditableRoleMapping[]>;
}
export interface IRoleMappingsSetAction {
type: RoleMappingActionTypes.SET_ROLE_MAPPINGS;
submitState: IAsyncStateContainer<string>;
}
export interface IRoleMappingSubmitUpdateAction {
type: RoleMappingActionTypes.SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE;
submitState: IAsyncStateContainer<string>;
}
export type RoleMappingActions = IRoleMappingsGetAllAction;
export const getAllRoleMappings: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IRoleMappingsGetAllAction>
> = () => {
return async (dispatch: Dispatch<IRoleMappingsGetAllAction>) => {
dispatch({
type: RoleMappingActionTypes.GET_ALL_ROLE_MAPPINGS,
roleMappings: { state: AsyncState.Loading }
});
const allRoleMappings = await RoleMappingService.getAllRoleMappings();
dispatch({
type: RoleMappingActionTypes.GET_ALL_ROLE_MAPPINGS,
roleMappings: allRoleMappings
});
};
};
export const submitRoleMappingUpdate: ActionCreator<
ThunkAction<Promise<any>, IAppState, null, IRoleMappingSubmitUpdateAction>
> = (roleMapping: IRoleMappingChange) => {
return async (dispatch: Dispatch<IRoleMappingSubmitUpdateAction>) => {
dispatch({
type: RoleMappingActionTypes.SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE,
submitState: { state: AsyncState.Loading }
});
const postState = await RoleMappingService.assignNewRoleMapping(
roleMapping
);
dispatch({
type: RoleMappingActionTypes.SUBMIT_ROLE_MAPPING_ASSIGNMENT_UPDATE,
submitState: postState
});
};
};
@@ -0,0 +1,43 @@
import { Dispatch } from "react";
import { ActionCreator } from "redux";
import { ThunkAction } from "redux-thunk";
import { IUserAssignmentState } from "../reducers/UserAssignmentReducer";
import AsyncState from "../shared/enumerations/AsyncState";
import { IUserAssignment } from "../shared/models/UserAssignment";
import IAsyncStateContainer from "../shared/types/IAsyncStateContainer";
import UserAssignmentService from "../shared/services/UserAssignmentService";
export enum UserAssignmentActionTypes {
GET_ALL_USER_ASSIGNMENTS = "GET_ALL_USER_ASSIGNMENTS",
SET_REGIONAL_MANAGER = "SET_REGIONAL_MANAGER"
}
export interface IUserAssignmentGetAllAction {
type: UserAssignmentActionTypes.GET_ALL_USER_ASSIGNMENTS;
userAssignments: IAsyncStateContainer<IUserAssignment[]>;
}
export type UserAssignmentActions = IUserAssignmentGetAllAction;
export const getAllUserAssignments: ActionCreator<
ThunkAction<
Promise<any>,
IUserAssignmentState,
null,
IUserAssignmentGetAllAction
>
> = () => {
return async (dispatch: Dispatch<IUserAssignmentGetAllAction>) => {
dispatch({
type: UserAssignmentActionTypes.GET_ALL_USER_ASSIGNMENTS,
userAssignments: { state: AsyncState.Loading }
});
const retrievedAssignments = await UserAssignmentService.getAllUserAssignments();
dispatch({
type: UserAssignmentActionTypes.GET_ALL_USER_ASSIGNMENTS,
userAssignments: retrievedAssignments
});
};
};
@@ -0,0 +1,23 @@
import { RedirectRequest } from "@azure/msal-browser";
export const loginConfig: RedirectRequest = {
scopes: ["User.Read"]
};
export const tokenConfig: RedirectRequest = {
scopes: ["e9cb601e-e98d-4881-90fc-d5758dac5ec8/.default"]
};
export const graphConfig = {
graphMeEndpoint: "https://graph.microsoft-ppe.com/v1.0/me"
};
export const securityConfig = {
auth: {
clientId: "e9cb601e-e98d-4881-90fc-d5758dac5ec8",
authority:
"https://login.microsoftonline.com/63cd1554-2783-4a86-ae50-b9e6c21c9499",
redirectUri: "/incentive-manager",
postLogoutRedirectUri: "/"
}
};
@@ -0,0 +1,91 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import Product2 from "../../shared/enumerations/Product";
import {
ActivityState,
ActivityType,
IAuditableActivity,
SystemType
} from "../../shared/models/Activity";
import { UserType } from "../../shared/models/Auditable";
import ActivityApproval from "./ActivityApproval";
describe("ActivityApproval", () => {
describe("User has permission to approve activity", () => {
// you don't need all of the old tests, because the front end doesn't do this logic anymore.
it("should display buttons when permissions present", () => {
const activity: IAuditableActivity = mockActivity();
activity.type = ActivityType.Referral;
activity.state = ActivityState.ManualReview;
const mockAuth = {
canApproveTransferTo: true,
canApproveTransferFrom: true,
allowedChanges: [ActivityState.Accepted, ActivityState.Rejected]
};
const wrapper = shallow(
<ActivityApproval
authorization={mockAuth}
activity={activity}
onSetActivityState={jest.fn()}
/>
);
expect(
wrapper.find("[data-test='Accepted-activity-button']").length
).toBe(1);
expect(
wrapper.find("[data-test='Rejected-activity-button']").length
).toBe(1);
});
it("should not display buttons when not present", () => {
const activity: IAuditableActivity = mockActivity();
activity.type = ActivityType.Referral;
activity.state = ActivityState.ManualReview;
const mockAuth = {
canApproveTransferTo: true,
canApproveTransferFrom: true,
allowedChanges: [ActivityState.Accepted]
};
const wrapper = shallow(
<ActivityApproval
authorization={mockAuth}
activity={activity}
onSetActivityState={jest.fn()}
/>
);
expect(
wrapper.find("[data-test='Accepted-activity-button']").length
).toBe(1);
expect(
wrapper.find("[data-test='Rejected-activity-button']").length
).toBe(0);
});
});
});
const mockActivity = (): IAuditableActivity => {
return {
id: 1,
product: Product2.Mortgage,
type: ActivityType.Referral,
sourceSystem: SystemType.Dna,
sourceId: "11",
sourceCreated: moment.utc(),
departmentId: "25",
employeeId: "11771",
employeeEmail: "[email protected]",
accepted: moment.utc(),
state: ActivityState.Accepted,
created: moment.utc(),
createdBy: "Created Doe",
createdByType: UserType.Associate,
lastUpdated: moment.utc(),
lastUpdatedBy: "Updated Doe",
lastUpdatedByType: UserType.Associate,
detail: {},
activityPayouts: []
};
};
@@ -0,0 +1,90 @@
import React from "react";
import { Button } from "reactstrap";
import {
ActivityState,
ActivityStateTitles,
IAuditableActivity,
IAuthorizedActivityCommands,
ManualActivityState
} from "../../shared/models/Activity";
import moment, { Moment } from "moment";
import PermissionGuard from "../../containers/shared/PermissionGuard";
import PermissionCode from "../../shared/enumerations/PermissionCode";
interface IActivityApprovalProps {
activity: IAuditableActivity;
onSetActivityState: (
newState: ActivityState,
newDate: Moment | undefined
) => void;
authorization: IAuthorizedActivityCommands;
}
const ActivityApproval: React.FC<IActivityApprovalProps> = (
props: IActivityApprovalProps
) => {
if (
props.activity.activityPayouts.length > 0 ||
props.authorization.allowedChanges?.length === 0
) {
return <></>;
}
const createActionButton = (
state: ActivityState,
newDate: Moment | undefined
) => {
return (
<tr>
<PermissionGuard
requiredPermissions={[
PermissionCode.SET_ACTIVITY_ACCEPTED_DATE
]}
>
<td>
<input
type="date"
onKeyDown={(e) => e.preventDefault()}
data-test="accepted-date-input"
min={moment(moment.now())
.subtract(1, "months")
.startOf("month")
.format("YYYY-MM-DD")}
max={moment(moment.now())
.add(1, "months")
.format("YYYY-MM-DD")}
onChange={(event) =>
(newDate = moment(event.target.value) ?? "")
}
/>
</td>
</PermissionGuard>
<td>
<Button
data-test={state + "-activity-button"}
className="d-inline mr-2 mt-1"
outline
color="success"
onClick={() => props.onSetActivityState(state, newDate)}
>
{ActivityStateTitles[state]}
</Button>
</td>
</tr>
);
};
const manualActions = Object.keys(ManualActivityState);
const availableActions = props.authorization.allowedChanges.map(
(allowed) => {
return (
manualActions.includes(allowed) &&
createActionButton(allowed, undefined)
);
}
);
return <>{availableActions}</>;
};
export default ActivityApproval;
@@ -0,0 +1,162 @@
import { shallow } from "enzyme";
import React from "react";
import AsyncState from "../../shared/enumerations/AsyncState";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import ActivityDetailAlertBar from "./ActivityDetailAlertBar";
describe("Activity Detail Alert Bar", () => {
it("should return activity failure alert if activity state is failure", () => {
const activity: IAsyncStateContainer<any> = {
state: AsyncState.Failure,
data: "Activity Failed message"
};
const activityChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const activityTransferChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const wrapper = shallow(
<ActivityDetailAlertBar
activity={activity}
activityStateChange={activityChange}
activityTransferChange={activityTransferChange}
/>
);
wrapper.debug();
expect(
wrapper.find("[data-test='activity-failure-alert']").length
).toBe(1);
expect(
wrapper
.find("[data-test='activity-failure-alert']")
.childAt(0)
.text()
).toEqual("Activity Failed message");
});
it("should return activity changes failure alert if activity change is failure", () => {
const activity: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const activityChange: IAsyncStateContainer<any> = {
state: AsyncState.Failure,
data: "Activity Change Failed message"
};
const activityTransferChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const wrapper = shallow(
<ActivityDetailAlertBar
activity={activity}
activityStateChange={activityChange}
activityTransferChange={activityTransferChange}
/>
);
expect(wrapper.find("[data-test='activity-change-alert']").length).toBe(
1
);
expect(
wrapper
.find("[data-test='activity-change-alert']")
.childAt(0)
.text()
).toEqual("Activity Change Failed message");
});
it("should return loading alert if activity details are loading", () => {
const activity: IAsyncStateContainer<any> = {
state: AsyncState.Loading
};
const activityChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const activityTransferChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const wrapper = shallow(
<ActivityDetailAlertBar
activity={activity}
activityStateChange={activityChange}
activityTransferChange={activityTransferChange}
/>
);
expect(
wrapper.find("[data-test='activity-loading-alert']").length
).toBe(1);
});
it("should return loading alert if activity state change is loading", () => {
const activity: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const activityChange: IAsyncStateContainer<any> = {
state: AsyncState.Loading,
data: {}
};
const activityTransferChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const wrapper = shallow(
<ActivityDetailAlertBar
activity={activity}
activityStateChange={activityChange}
activityTransferChange={activityTransferChange}
/>
);
expect(
wrapper.find("[data-test='activity-loading-alert']").length
).toBe(1);
});
it("should return an empty fragment if no async states are failure", () => {
const activity: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const activityChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const activityTransferChange: IAsyncStateContainer<any> = {
state: AsyncState.Success,
data: {}
};
const wrapper = shallow(
<ActivityDetailAlertBar
activity={activity}
activityStateChange={activityChange}
activityTransferChange={activityTransferChange}
/>
);
expect(wrapper.length).toBe(1);
});
});
@@ -0,0 +1,62 @@
import React from "react";
import { Alert } from "reactstrap";
import IAsyncStateContainer from "../../shared/types/IAsyncStateContainer";
import {
IAuditableActivity,
IActivityTransferAction,
IViewableActivityDetail
} from "../../shared/models/Activity";
import AsyncState from "../../shared/enumerations/AsyncState";
import Translate from "../../translate/Translate";
interface IActivityDetailAlertBarProps {
activity: IAsyncStateContainer<IViewableActivityDetail>;
activityStateChange: IAsyncStateContainer<IAuditableActivity>;
activityTransferChange: IAsyncStateContainer<IActivityTransferAction>;
}
const alertTranlate =
Translate.Get.components.activityDetails.activityDetailAlertBar;
const ActivityDetailAlertBar: React.FC<IActivityDetailAlertBarProps> = (
props: IActivityDetailAlertBarProps
) => {
if (props.activityStateChange.state === AsyncState.Failure) {
return (
<Alert color="danger" data-test="activity-change-alert">
{props.activityStateChange.data}
</Alert>
);
}
if (props.activity.state === AsyncState.Failure) {
return (
<Alert color="danger" data-test="activity-failure-alert">
{props.activity.data}
</Alert>
);
}
if (props.activityTransferChange.state === AsyncState.Failure) {
return (
<Alert color="danger" data-test="activity-transfer-change-alert">
{props.activityTransferChange.data}
</Alert>
);
}
if (
props.activity.state === AsyncState.Loading ||
props.activityStateChange.state === AsyncState.Loading ||
props.activityTransferChange.state === AsyncState.Loading
) {
return (
<Alert color="warning" data-test="activity-loading-alert">
{alertTranlate.loadingMessage}
</Alert>
);
}
return <></>;
};
export default ActivityDetailAlertBar;
@@ -0,0 +1,24 @@
import { shallow } from "enzyme";
import React from "react";
import { IActivityDetail } from "../../shared/models/Activity";
import ActivityDetailDisplay from "./ActivityDetailDisplay";
describe("ActivityDetailDisplay", () => {
it("should display activity detail table", () => {
const detail = {
id: 1,
accountNumber: "11",
memberNumber: "12",
memberName: "Member Doe",
accountStatus: "Open",
accountBalance: 21,
accountNewBalance: 11
} as IActivityDetail;
const wrapper = shallow(<ActivityDetailDisplay detail={detail} />);
expect(
wrapper.find("[data-test='activity-detail-display']").length
).toBe(1);
});
});
@@ -0,0 +1,145 @@
import moment from "moment";
import React from "react";
import { IActivityDetail } from "../../shared/models/Activity";
import Translate from "../../translate/Translate";
import GoalService from "../../shared/services/GoalService";
import { IDeadActivityDetail } from "../../shared/models/DeadActivity";
interface IActivityDetailDisplayProps {
detail: IActivityDetail | IDeadActivityDetail;
}
const detailTranslate =
Translate.Get.components.activityDetails.activityDetailDisplay;
const ActivityDetailDisplay: React.FC<IActivityDetailDisplayProps> = (
props: IActivityDetailDisplayProps
) => {
return (
<div data-test="activity-detail-display">
<h5>{detailTranslate.title}</h5>
<table className="table table-responsive">
<tbody>
<tr>
<td>{detailTranslate.headers.id}</td>
<td>{props.detail.id}</td>
</tr>
<tr>
<td>{detailTranslate.headers.dnaPersonNumber}</td>
<td>{props.detail.dnaPersonNbr}</td>
</tr>
<tr>
<td>{detailTranslate.headers.accountNumber}</td>
<td>{props.detail.accountNumber}</td>
</tr>
<tr>
<td>{detailTranslate.headers.subAccountNumber}</td>
<td>{props.detail.subAccountNumber}</td>
</tr>
<tr>
<td>{detailTranslate.headers.subjectPersonNbr}</td>
<td>{props.detail.subjectPersonNbr}</td>
</tr>
<tr>
<td>{detailTranslate.headers.subjectOrgNbr}</td>
<td>{props.detail.subjectOrgNbr}</td>
</tr>
<tr>
<td>{detailTranslate.headers.memberName}</td>
<td>{props.detail.memberName}</td>
</tr>
<tr>
<td>{detailTranslate.headers.stationName}</td>
<td>{props.detail.stationName}</td>
</tr>
<tr>
<td>{detailTranslate.headers.branchOrgNbr}</td>
<td>{props.detail.branchOrgNbr}</td>
</tr>
<tr>
<td>{detailTranslate.headers.branchOrgName}</td>
<td>{props.detail.branchOrgName}</td>
</tr>
<tr>
<td>{detailTranslate.headers.accountStatus}</td>
<td>{props.detail.accountStatus}</td>
</tr>
<tr>
<td>{detailTranslate.headers.accountBalance}</td>
<td>
{GoalService.formatMonetary(
props.detail.accountBalance
)}
</td>
</tr>
<tr>
<td>{detailTranslate.headers.debtProtectionAddDate}</td>
<td>
{props.detail.debtProtectionAddDate
? moment(
props.detail.debtProtectionAddDate
).format("MM-DD-YYYY")
: ""}
</td>
</tr>
<tr>
<td>
{detailTranslate.headers.debtProtectionRemoveDate}
</td>
<td>
{props.detail.debtProtectionRemoveDate
? moment(
props.detail.debtProtectionRemoveDate
).format("MM-DD-YYYY")
: ""}
</td>
</tr>
<tr>
<td>
{detailTranslate.headers.isDebtProtectionEligible}
</td>
<td>
{props.detail.isDebtProtectionEligible
? "True"
: "False"}
</td>
</tr>
<tr>
<td>{detailTranslate.headers.gapInsuranceAddDate}</td>
<td>
{props.detail.gapInsuranceAddDate
? moment(
props.detail.gapInsuranceAddDate
).format("MM-DD-YYYY")
: ""}
</td>
</tr>
<tr>
<td>
{detailTranslate.headers.gapInsuranceRemoveDate}
</td>
<td>
{props.detail.gapInsuranceRemoveDate
? moment(
props.detail.gapInsuranceRemoveDate
).format("MM-DD-YYYY")
: ""}
</td>
</tr>
<tr>
<td>
{detailTranslate.headers.isGapInsuranceEligible}
</td>
<td>
{props.detail.isGapInsuranceEligible
? "True"
: "False"}
</td>
</tr>
</tbody>
</table>
</div>
);
};
export default ActivityDetailDisplay;
@@ -0,0 +1,56 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import { IActivityDetailUpdate } from "../../shared/models/Activity";
import ActivityDetailUpdatesTable from "./ActivityDetailUpdatesTable";
describe("ActivityDetailUpdatesTable", () => {
it("should display the changes in a table", () => {
const changes = [
{
id: 1,
note: "Account Number change",
created: moment.utc()
},
{
id: 2,
note: "Account Number change",
created: moment.utc()
}
] as IActivityDetailUpdate[];
const wrapper = shallow(
<ActivityDetailUpdatesTable activityDetailUpdates={changes} />
);
expect(
wrapper.find("[data-test='activity-detail-updates-table']").length
).toBe(1);
// table rows
expect(
wrapper.find("[data-test='activity-detail-updates-table'] tbody tr")
.length
).toBe(2);
// alert component
expect(
wrapper.find("[data-test='activity-detail-updates-table-alert']")
.length
).toBe(0);
});
it("should display an alert if there are no changes", () => {
const wrapper = shallow(
<ActivityDetailUpdatesTable activityDetailUpdates={[]} />
);
// alert component
expect(
wrapper.find("[data-test='activity-detail-updates-table-alert']")
.length
).toBe(1);
//table
expect(
wrapper.find("[data-test='activity-detail-updates-table'] table")
.length
).toBe(0);
});
});
@@ -0,0 +1,56 @@
import moment from "moment";
import React from "react";
import { Alert } from "reactstrap";
import { IActivityDetailUpdate } from "../../shared/models/Activity";
import Translate from "../../translate/Translate";
interface IActivityStateChangesTableProps {
activityDetailUpdates: IActivityDetailUpdate[];
}
const tableTranslate =
Translate.Get.components.activityDetails.activityDetailUpdatesTable;
const ActivityStateChangesTable: React.FC<IActivityStateChangesTableProps> = (
props: IActivityStateChangesTableProps
) => {
return (
<div data-test="activity-detail-updates-table">
<h5>{tableTranslate.title}</h5>
{props.activityDetailUpdates.length ? (
<table className="table table-responsive">
<thead>
<tr>
<th>{tableTranslate.headers.note}</th>
<th>{tableTranslate.headers.created}</th>
<th>{tableTranslate.headers.createdBy}</th>
</tr>
</thead>
<tbody>
{props.activityDetailUpdates.map((change) => (
<tr key={change.id}>
<td>{change.note}</td>
<td>
{moment
.utc(change.created)
.local()
.format("lll")}
</td>
<td>{change.createdBy}</td>
</tr>
))}
</tbody>
</table>
) : (
<Alert
color="info"
data-test="activity-detail-updates-table-alert"
>
{tableTranslate.noChangesMessage}
</Alert>
)}
</div>
);
};
export default ActivityStateChangesTable;
@@ -0,0 +1,57 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import {
ActivityState,
ActivityType,
IAuditableActivity,
SystemType
} from "../../shared/models/Activity";
import { UserType } from "../../shared/models/Auditable";
import ActivityDisplay from "./ActivityDisplay";
import { mockProfileState } from "../../shared/testing/mocks/MockProfileState";
import Product2 from "../../shared/enumerations/Product";
describe("ActivityDisplay", () => {
it("should display the activity table", () => {
const activity: IAuditableActivity = {
employeeEmail: "",
employeeId: "",
id: 1,
product: Product2.ATMCard,
type: ActivityType.Referral,
sourceSystem: SystemType.Dna,
sourceId: "11",
sourceCreated: moment.utc(),
departmentId: "25",
accepted: moment.utc(),
state: ActivityState.Accepted,
created: moment.utc(),
createdBy: "Created Doe",
createdByType: UserType.Associate,
lastUpdated: moment.utc(),
lastUpdatedBy: "Updated Doe",
lastUpdatedByType: UserType.Associate,
detail: {},
activityPayouts: []
};
let callCount = 0;
const onSetState = () => {
callCount++;
};
const wrapper = shallow(
<ActivityDisplay
activity={activity}
profile={mockProfileState.profile.data!}
onSetActivityState={onSetState}
/>
);
expect(wrapper.find("[data-test='activity-display']").length).toBe(1);
expect(wrapper.find("[data-test='activity-display'] tr").length).toBe(
12
);
});
});
@@ -0,0 +1,128 @@
import moment, { Moment } from "moment";
import React from "react";
import {
ActivityState,
IAuditableActivity,
IAuthorizedActivityCommands
} from "../../shared/models/Activity";
import Translate from "../../translate/Translate";
import ActivityApproval from "./ActivityApproval";
import { IEmployee } from "../../shared/models/Employee";
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
import UserDisplay from "../../containers/shared/UserDisplay";
import DateTimeService from "../../shared/services/DateTimeService";
interface IActivityDisplayProps {
activity: IAuditableActivity;
onSetActivityState: (
newState: ActivityState,
newDate: Moment | undefined
) => void;
profile: IEmployee;
authorization: IAuthorizedActivityCommands;
}
const activityTranslate =
Translate.Get.components.activityDetails.activityDisplay;
const ActivityDisplay: React.FC<IActivityDisplayProps> = (
props: IActivityDisplayProps
) => {
return (
<div data-test="activity-display">
<h5>{activityTranslate.title}</h5>
<table className="table table-responsive">
<tbody>
<tr>
<td>{activityTranslate.headers.id}</td>
<td>{props.activity.id}</td>
</tr>
<tr>
<td>{activityTranslate.headers.product}</td>
<td>{props.activity.product}</td>
</tr>
<tr>
<td>{activityTranslate.headers.type}</td>
<td>{props.activity.type}</td>
</tr>
<tr>
<td>{activityTranslate.headers.sourceSystem}</td>
<td>{props.activity.sourceSystem}</td>
</tr>
<tr>
<td>{activityTranslate.headers.sourceId}</td>
<td>{props.activity.sourceId}</td>
</tr>
<tr>
<td>{activityTranslate.headers.sourceCreated}</td>
<td>
{DateTimeService.CastServerTimeToClientTimeString(
props.activity.sourceCreated
)}
</td>
</tr>
<tr>
<td>{activityTranslate.headers.departmentId}</td>
<td>
<DepartmentDisplay
departmentId={parseInt(
props.activity.departmentId,
10
)}
/>
</td>
</tr>
<tr>
<td>{activityTranslate.headers.employeeId}</td>
<td>
<UserDisplay
employeeId={props.activity.employeeId}
/>
</td>
</tr>
<tr>
<td>{activityTranslate.headers.accepted}</td>
<td>
{DateTimeService.CastServerTimeToClientTimeString(
props.activity.accepted
)}
</td>
</tr>
<tr>
<td>{activityTranslate.headers.state}</td>
<td>{props.activity.state}</td>
</tr>
{props.activity.activityPayouts.length === 0 && (
<ActivityApproval
activity={props.activity}
onSetActivityState={(newState, newDate) =>
props.onSetActivityState(newState, newDate)
}
authorization={props.authorization}
/>
)}
<tr>
<td>{activityTranslate.headers.createdBy}</td>
<td title={props.activity.createdByType}>
{props.activity.createdBy} on{" "}
{DateTimeService.CastServerTimeToClientTimeString(
props.activity.created
)}
</td>
</tr>
<tr>
<td>{activityTranslate.headers.lastUpdatedBy}</td>
<td title={props.activity.lastUpdatedByType}>
{props.activity.lastUpdatedBy} on{" "}
{DateTimeService.CastServerTimeToClientTimeString(
props.activity.lastUpdated
)}
</td>
</tr>
</tbody>
</table>
</div>
);
};
export default ActivityDisplay;
@@ -0,0 +1,45 @@
import React from "react";
import Translate from "../../translate/Translate";
import { IMetaDataLineItem } from "../../shared/models/Activity";
import { Alert } from "reactstrap";
interface IActivityMetaDataDisplayProps {
activityMetaData: IMetaDataLineItem[];
}
const metaDataTranslate =
Translate.Get.components.activityDetails.activityMetaDataDisplay;
const ActivityMetaDataDisplay: React.FC<IActivityMetaDataDisplayProps> = (
props: IActivityMetaDataDisplayProps
) => {
return (
<div data-test="activity-metadata-display">
<h5>{metaDataTranslate.title}</h5>
{props.activityMetaData.length ? (
<table className="table">
<thead>
<tr>
<th>{metaDataTranslate.headers.metaData}</th>
<th>{metaDataTranslate.headers.metaDataType}</th>
</tr>
</thead>
<tbody>
{props.activityMetaData.map((data) => (
<tr key={data.id}>
<td>{data.metaData}</td>
<td>{data.metaDataType}</td>
</tr>
))}
</tbody>
</table>
) : (
<Alert color="info" data-test="no-metaData-alert">
{metaDataTranslate.noMetaDataMessage}
</Alert>
)}
</div>
);
};
export default ActivityMetaDataDisplay;
@@ -0,0 +1,60 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import { IPayoutLineItem } from "../../shared/models/Payout";
import ActivityPayoutsDisplay from "./ActivityPayoutsDisplay";
describe("ActivityPayoutsDisplay", () => {
it("should display the payouts in a table", () => {
const payouts: IPayoutLineItem[] = [
{
activityPeriod: moment.utc(),
departmentId: 25,
role: {
description: "Teller",
name: "Teller Doe",
permissions: [],
hasCommissions: true
},
employeeId: "123",
activityTotal: 100,
payoutTotal: 200
},
{
activityPeriod: moment.utc(),
departmentId: 25,
role: {
description: "Branch Manager",
name: "B.M. Doe",
permissions: [],
hasCommissions: true
},
employeeId: "456",
activityTotal: 100,
payoutTotal: 200
}
] as IPayoutLineItem[];
const wrapper = shallow(<ActivityPayoutsDisplay payouts={payouts} />);
expect(
wrapper.find("[data-test='activity-payouts-display']").length
).toBe(1);
// table rows
expect(
wrapper.find("[data-test='activity-payouts-display'] tbody tr")
.length
).toBe(2);
// alert component
expect(wrapper.find("[data-test='no-payouts-alert']").length).toBe(0);
});
it("should display an alert if there are no payouts", () => {
const wrapper = shallow(<ActivityPayoutsDisplay payouts={[]} />);
// alert component
expect(wrapper.find("[data-test='no-payouts-alert']").length).toBe(1);
//table
expect(
wrapper.find("[data-test='activity-payouts-display'] table").length
).toBe(0);
});
});
@@ -0,0 +1,58 @@
import React from "react";
import Translate from "../../translate/Translate";
import { IPayoutLineItem } from "../../shared/models/Payout";
import moment from "moment";
import { Alert } from "reactstrap";
interface IActivityPayoutsDisplayProps {
payouts: IPayoutLineItem[];
}
const payoutsTranslate =
Translate.Get.components.activityDetails.activityPayoutsDisplay;
const ActivityPayoutsDisplay: React.FC<IActivityPayoutsDisplayProps> = (
props: IActivityPayoutsDisplayProps
) => {
return (
<div data-test="activity-payouts-display">
<h5>{payoutsTranslate.title}</h5>
{props.payouts.length ? (
<table className="table">
<thead>
<tr>
<th>{payoutsTranslate.headers.period}</th>
<th>{payoutsTranslate.headers.departmentId}</th>
<th>{payoutsTranslate.headers.role}</th>
<th>{payoutsTranslate.headers.employeeId}</th>
<th>{payoutsTranslate.headers.activityTotal}</th>
<th>{payoutsTranslate.headers.payoutTotal}</th>
</tr>
</thead>
<tbody>
{props.payouts.map((payout) => (
<tr key={payout.id}>
<td>
{moment
.utc(payout.activityPeriod)
.format("ll")}
</td>
<td>{payout.departmentId}</td>
<td>{payout.role?.description}</td>
<td>{payout.employeeId}</td>
<td>{payout.activityTotal}</td>
<td>{payout.payoutTotal}</td>
</tr>
))}
</tbody>
</table>
) : (
<Alert color="info" data-test="no-payouts-alert">
{payoutsTranslate.noPayoutsMessage}
</Alert>
)}
</div>
);
};
export default ActivityPayoutsDisplay;
@@ -0,0 +1,58 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import { IActivityStateChange } from "../../shared/models/Activity";
import ActivityStateChangesTable from "./ActivityStateChangesTable";
describe("ActivityStateChangesTable", () => {
it("should display the changes in a table", () => {
const changes = [
{
id: 1,
state: "Created",
note: "Auto created",
created: moment.utc()
},
{
id: 2,
state: "Created",
note: "Auto created",
created: moment.utc()
}
] as IActivityStateChange[];
const wrapper = shallow(
<ActivityStateChangesTable activityStateChanges={changes} />
);
expect(
wrapper.find("[data-test='activity-state-changes-table']").length
).toBe(1);
// table rows
expect(
wrapper.find("[data-test='activity-state-changes-table'] tbody tr")
.length
).toBe(2);
// alert component
expect(
wrapper.find("[data-test='activity-state-changes-table-alert']")
.length
).toBe(0);
});
it("should display an alert if there are no changes", () => {
const wrapper = shallow(
<ActivityStateChangesTable activityStateChanges={[]} />
);
// alert component
expect(
wrapper.find("[data-test='activity-state-changes-table-alert']")
.length
).toBe(1);
//table
expect(
wrapper.find("[data-test='activity-state-changes-table'] table")
.length
).toBe(0);
});
});
@@ -0,0 +1,83 @@
import moment from "moment";
import React from "react";
import { Alert } from "reactstrap";
import { IActivityStateChange } from "../../shared/models/Activity";
import Translate from "../../translate/Translate";
import Configuration from "../../shared/configuration/Configuration";
interface IActivityStateChangesTableProps {
activityStateChanges: IActivityStateChange[];
}
const tableTranslate =
Translate.Get.components.activityDetails.activityStateChangesTable;
const urlPrefix = Configuration.PrefixAppUrlWithBasename(
Configuration.Get.appUrls.activity
);
const ActivityStateChangesTable: React.FC<IActivityStateChangesTableProps> = (
props: IActivityStateChangesTableProps
) => {
// If there is a matching activity, make it a link.
const getNote = (change: IActivityStateChange) => {
if (change.activityMatchId) {
return (
<a href={urlPrefix + "/" + change.activityMatchId}>
{change.note}
</a>
);
}
return change.note;
};
return (
<div data-test="activity-state-changes-table">
<h5>{tableTranslate.title}</h5>
{props.activityStateChanges.length ? (
<table className="table table-responsive">
<thead>
<tr>
<th>{tableTranslate.headers.state}</th>
<th>{tableTranslate.headers.accepted}</th>
<th>{tableTranslate.headers.note}</th>
<th>{tableTranslate.headers.created}</th>
<th>{tableTranslate.headers.createdBy}</th>
</tr>
</thead>
<tbody>
{props.activityStateChanges.map((change) => (
<tr key={change.id}>
<td>{change.state}</td>
<td>
{change.acceptedDate == null
? ""
: moment(change.acceptedDate).format(
"ll"
)}
</td>
<td>{getNote(change)}</td>
<td>
{moment
.utc(change.created)
.local()
.format("lll")}
</td>
<td>{change.createdBy}</td>
</tr>
))}
</tbody>
</table>
) : (
<Alert
color="info"
data-test="activity-state-changes-table-alert"
>
{tableTranslate.noChangesMessage}
</Alert>
)}
</div>
);
};
export default ActivityStateChangesTable;
@@ -0,0 +1,263 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import PermissionCode from "../../shared/enumerations/PermissionCode";
import Product2 from "../../shared/enumerations/Product";
import {
ActivityState,
ActivityTransferState,
ActivityType,
IActivityTransferAction,
IAuditableActivity,
SystemType
} from "../../shared/models/Activity";
import { UserType } from "../../shared/models/Auditable";
import { IEmployee } from "../../shared/models/Employee";
import ActivityTransferApproval from "./ActivityTransferApproval";
describe("ActivityTransferApproval", () => {
it("should not display if the transfer action is undefined", () => {
const mockAuth = {
canApproveTransferTo: true,
canApproveTransferFrom: true,
allowedChanges: []
};
const wrapper = shallow(
<ActivityTransferApproval
transferAction={undefined}
authorization={mockAuth}
activity={mockActivity()}
approveTransfer={jest.fn()}
/>
);
expect(
wrapper.find("[data-test='activity-approval-component']").length
).toBe(0);
});
it("should display if the transfer action exists", () => {
const userProfile = mockProfile();
userProfile.departments = [];
const mockAuth = {
canApproveTransferTo: false,
canApproveTransferFrom: false,
allowedChanges: []
};
const wrapper = shallow(
<ActivityTransferApproval
transferAction={mockActivityTransferAction()}
authorization={mockAuth}
activity={mockActivity()}
approveTransfer={jest.fn()}
/>
);
expect(
wrapper.find("[data-test='activity-approval-component']").length
).toBe(1);
expect(
wrapper.find("[data-test='approve-transfer-button']").length
).toBe(0);
expect(
wrapper.find("[data-test='reject-transfer-button']").length
).toBe(0);
});
it("should display approval buttons if profile has a department manager department assignment", () => {
const mockAuth = {
canApproveTransferTo: true,
canApproveTransferFrom: true,
allowedChanges: []
};
const wrapper = shallow(
<ActivityTransferApproval
transferAction={mockActivityTransferAction()}
authorization={mockAuth}
activity={mockActivity()}
approveTransfer={jest.fn()}
/>
);
expect(
wrapper.find("[data-test='approve-transfer-button']").length
).toBe(1);
expect(
wrapper.find("[data-test='reject-transfer-button']").length
).toBe(1);
});
it("should display approval buttons if profile has a department manager department assignment", () => {
const mockAuth = {
canApproveTransferTo: true,
canApproveTransferFrom: true,
allowedChanges: []
};
const wrapper = shallow(
<ActivityTransferApproval
transferAction={mockActivityTransferAction()}
authorization={mockAuth}
activity={mockActivity()}
approveTransfer={jest.fn()}
/>
);
expect(
wrapper.find("[data-test='approve-transfer-button']").length
).toBe(1);
expect(
wrapper.find("[data-test='reject-transfer-button']").length
).toBe(1);
});
it("should not display approval buttons if profile has been approved for the department", () => {
const mockAuth = {
canApproveTransferTo: false,
canApproveTransferFrom: false,
allowedChanges: []
};
const activityFromDept25 = mockActivity();
// set up transfer approved by the appUser's department
const transferAction = mockActivityTransferAction();
transferAction.newActivityDepartmentId = 1;
transferAction.requestedDepartmentApprover = "Aaron Manager";
const wrapper = shallow(
<ActivityTransferApproval
transferAction={transferAction}
authorization={mockAuth}
activity={activityFromDept25}
approveTransfer={() => jest.fn()}
/>
);
expect(
wrapper.find("[data-test='approve-transfer-button']").length
).toBe(0);
expect(
wrapper.find("[data-test='reject-transfer-button']").length
).toBe(0);
});
it("should call the approveTransfer function when buttons are clicked", () => {
const mockAuth = {
canApproveTransferTo: true,
canApproveTransferFrom: true,
allowedChanges: []
};
const approveTransferSpy = jest.fn();
const wrapper = shallow(
<ActivityTransferApproval
transferAction={mockActivityTransferAction()}
authorization={mockAuth}
activity={mockActivity()}
approveTransfer={approveTransferSpy}
/>
);
wrapper.find("[data-test='approve-transfer-button']").simulate("click");
expect(approveTransferSpy).toHaveBeenCalledWith(
11,
ActivityTransferState.Accepted
);
expect(approveTransferSpy).toHaveBeenCalledTimes(1);
wrapper.find("[data-test='reject-transfer-button']").simulate("click");
expect(approveTransferSpy).toHaveBeenCalledWith(
11,
ActivityTransferState.Rejected
);
expect(approveTransferSpy).toHaveBeenCalledTimes(2);
});
});
const mockProfile = (): IEmployee => {
return {
adEmail: "[email protected]",
personId: 11,
employeeId: "11771",
homeDepartment: "HOME",
baseRole: "QA",
fullName: "John Tester",
role: {
name: "Admin",
description: "admin",
permissions: [
{
code: PermissionCode.ASSIGN_ASSOCIATES_TO_DEPARTMENT,
description: "ASSIGN_ASSOCIATES_TO_DEPARTMENT"
}
],
hasCommissions: false,
rank: 99
},
additionalPermissions: [],
departments: [
{
employeeId: "",
departmentId: 25,
startDate: moment(),
note: "",
isHome: true,
slidingPayout: 100,
role: {
name: "Manager",
description: "Manager",
hasCommissions: true,
permissions: [],
rank: 99
},
created: moment(),
createdBy: "",
createdByType: UserType.Associate,
lastUpdated: moment(),
lastUpdatedBy: "",
lastUpdatedByType: UserType.Associate
}
],
assignments: []
};
};
const mockActivity = (): IAuditableActivity => {
return {
id: 1,
product: ("ATMCard" as unknown) as Product2,
type: ActivityType.Referral,
sourceSystem: SystemType.Dna,
sourceId: "11",
sourceCreated: moment.utc(),
departmentId: "25",
employeeId: "2",
employeeEmail: "[email protected]",
accepted: moment.utc(),
state: ActivityState.Accepted,
created: moment.utc(),
createdBy: "Created Doe",
createdByType: UserType.Associate,
lastUpdated: moment.utc(),
lastUpdatedBy: "Updated Doe",
lastUpdatedByType: UserType.Associate,
detail: {
id: 1,
accountNumber: "11",
dnaPersonNbr: "12",
memberName: "Member Doe",
accountStatus: "Open",
accountBalance: 21,
accountNewBalance: 11
},
metaData: [],
activityPayouts: []
};
};
const mockActivityTransferAction = (): IActivityTransferAction => {
return {
id: 11,
activityStateBeforeTransfer: ActivityState.Accepted,
newActivityDepartmentId: 25,
newActivityEmployeeId: "C0002",
requestedActivityId: 1,
state: ActivityTransferState.Requested
};
};
@@ -0,0 +1,124 @@
import React from "react";
import { Button } from "reactstrap";
import DepartmentDisplay from "../../containers/shared/DepartmentDisplay";
import PermissionGuard from "../../containers/shared/PermissionGuard";
import PermissionCode from "../../shared/enumerations/PermissionCode";
import {
ActivityTransferState,
IActivity,
IActivityTransferAction,
IAuthorizedActivityCommands
} from "../../shared/models/Activity";
import Translate from "../../translate/Translate";
interface IActivityTransferApproval {
transferAction?: IActivityTransferAction;
activity: IActivity;
authorization: IAuthorizedActivityCommands;
approveTransfer: (
requestId: number,
decision: ActivityTransferState
) => void;
}
const approvalTranslate =
Translate.Get.components.activityDetails.activityTransferApproval;
const ActivityTransferApproval: React.FC<IActivityTransferApproval> = (
props: IActivityTransferApproval
) => {
const canApproveForCurrentDepartment =
props.authorization.canApproveTransferFrom;
const canApproveForNewDepartment = props.authorization.canApproveTransferTo;
return props.transferAction ? (
<div data-test="activity-approval-component">
<h5>{approvalTranslate.title}</h5>
<table className="table table-responsive">
<thead>
<tr>
<th>{approvalTranslate.headers.department}</th>
<th>{approvalTranslate.headers.employeeId}</th>
<th>
{approvalTranslate.headers.approvalFrom}
<DepartmentDisplay
departmentId={+props.activity.departmentId}
/>
</th>
<th>
{approvalTranslate.headers.approvalFrom}
<DepartmentDisplay
departmentId={
props.transferAction.newActivityDepartmentId
}
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<DepartmentDisplay
departmentId={
props.transferAction.newActivityDepartmentId
}
/>
</td>
<td>{props.transferAction.newActivityEmployeeId}</td>
<td>
{props.transferAction.requestedDepartmentApprover ||
approvalTranslate.pending}
</td>
<td>
{props.transferAction.newDepartmentApprover ||
approvalTranslate.pending}
</td>
</tr>
</tbody>
</table>
{(canApproveForCurrentDepartment || canApproveForNewDepartment) && (
<PermissionGuard
requiredPermissions={[
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_ASSOCIATES,
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS
]}
>
<div>
<Button
data-test="approve-transfer-button"
className="d-inline mr-2 mt-1"
outline
color="success"
onClick={() =>
props.approveTransfer(
props.transferAction!.id,
ActivityTransferState.Accepted
)
}
>
{approvalTranslate.buttons.approve}
</Button>
<Button
data-test="reject-transfer-button"
className="d-inline mt-1"
outline
color="danger"
onClick={() =>
props.approveTransfer(
props.transferAction!.id,
ActivityTransferState.Rejected
)
}
>
{approvalTranslate.buttons.reject}
</Button>
</div>
</PermissionGuard>
)}
</div>
) : (
<></>
);
};
export default ActivityTransferApproval;
@@ -0,0 +1,154 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import Product2 from "../../shared/enumerations/Product";
import {
ActivityState,
ActivityType,
IActivity,
SystemType
} from "../../shared/models/Activity";
import ActivityTransferRequest from "./ActivityTransferRequest";
describe("ActivityTransferRequest", () => {
it("should call props onFormSubmit with selected department and employee", () => {
const mockFormSubmit = jest.fn();
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.Accepted
};
const wrapper = shallow(
<ActivityTransferRequest
departments={[]}
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(
wrapper
.find("[data-test='activity-transfer-request-button']")
.prop("disabled")
).toBe(true);
wrapper
.find("UserSearchSelect")
.prop<(field: string, newValue: any) => void>("onUserSelected")(
"25",
"C002"
);
expect(
wrapper
.find("[data-test='activity-transfer-request-button']")
.prop("disabled")
).toBe(true);
wrapper.find("[data-test='dept-input']").prop<any>("onChange")({
target: { value: "25" }
});
expect(
wrapper
.find("[data-test='activity-transfer-request-button']")
.prop("disabled")
).toBe(false);
wrapper
.find("[data-test='activity-transfer-request-button']")
.simulate("click");
expect(mockFormSubmit).toHaveBeenCalledTimes(1);
expect(mockFormSubmit).toHaveBeenLastCalledWith("25", "C002");
});
it("should not display if the activity has payouts", () => {
const mockFormSubmit = jest.fn();
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [
{
payoutLineItem: {
id: 1,
activityPeriod: moment.utc(),
departmentId: 25,
role: {
description: "Teller",
name: "Teller Doe",
permissions: [],
hasCommissions: true,
rank: 1
},
product: "ATMCard",
goalId: 2,
goalValue: 3,
commissionId: 4,
commissionValue: 5,
employeeId: "123",
activityTotal: 100,
payoutTotal: 200,
slidingPayout: 12,
activityPayouts: undefined,
payoutProfileId: undefined
}
}
],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.Accepted
};
const wrapper = shallow(
<ActivityTransferRequest
departments={[]}
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(wrapper.find("[data-test='transfer-request-form']").length).toBe(
0
);
});
it("should not display if the activity is in PendingTransfer state", () => {
const mockFormSubmit = jest.fn();
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.PendingTransfer
};
const wrapper = shallow(
<ActivityTransferRequest
departments={[]}
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(wrapper.find("[data-test='transfer-request-form']").length).toBe(
0
);
});
});
@@ -0,0 +1,117 @@
import React, { useState } from "react";
import { Button, FormGroup, Input, Label } from "reactstrap";
import PermissionGuard from "../../containers/shared/PermissionGuard";
import PermissionCode from "../../shared/enumerations/PermissionCode";
import { IActivity } from "../../shared/models/Activity";
import { IDepartment } from "../../shared/models/Department";
import ActivityService from "../../shared/services/ActivityService";
import Translate from "../../translate/Translate";
import UserSearchSelect from "../activity/manual-activity-wizard/UserSearchSelect";
interface IActivityTransferRequestForm {
departmentId: string;
employeeId: string;
}
interface IActivityTransferRequestProps {
departments: IDepartment[];
activity: IActivity;
onFormSubmit: (departmentId: string, employeeId: string) => void;
}
const formTranslate =
Translate.Get.components.activityDetails.activityTransferRequest;
const ActivityTransferRequest: React.FC<IActivityTransferRequestProps> = (
props: IActivityTransferRequestProps
) => {
const [formState, setFormState] = useState<IActivityTransferRequestForm>({
departmentId: "",
employeeId: ""
});
const submitRequest = () => {
props.onFormSubmit(formState.departmentId, formState.employeeId);
};
const departments = props.departments;
// BUG? Did they tell you this is wrong and that it should work like approvals?
// OK. Just inject the activity.data.authorized from the parent and check if it can go to "PendingTransfer".
// For now they haven't asked for this, so still use the permission.
return ActivityService.isEligbleForTransfer(props.activity) ? (
<PermissionGuard
requiredPermissions={[
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_ASSOCIATES,
PermissionCode.TRANSFER_ACTIVITIES_BETWEEN_DEPARTMENTS
]}
>
<div data-test="transfer-request-form">
<h5>{formTranslate.title}</h5>
<FormGroup>
<Label>{formTranslate.userSearch.label}</Label>
<UserSearchSelect
data-test="appUser-search-select"
employeeId=""
onUserSelected={(_, newValue) => {
setFormState((prevState) => ({
departmentId: prevState.departmentId,
employeeId: newValue
}));
}}
showValidation={false}
/>
</FormGroup>
<FormGroup>
<Label>{formTranslate.department.label}</Label>
<Input
bsSize="sm"
type="select"
data-test="dept-input"
value={formState.departmentId}
onChange={(event) => {
const departmentId = event.target.value;
setFormState((prevState) => ({
departmentId,
employeeId: prevState.employeeId
}));
}}
>
<option
data-test="placeholder-option"
hidden={formState.departmentId !== ""}
key=""
value=""
>
{formTranslate.department.placeholder}
</option>
{departments.map((department) => {
return (
<option
key={department.id}
value={department.id.toString()}
>
{department.departmentName}
</option>
);
})}
</Input>
</FormGroup>
<Button
data-test="activity-transfer-request-button"
color="primary"
onClick={submitRequest}
disabled={
formState.employeeId.length === 0 ||
isNaN(parseInt(formState.departmentId))
}
>
{formTranslate.submitButton}
</Button>
</div>
</PermissionGuard>
) : (
<></>
);
};
export default ActivityTransferRequest;
@@ -0,0 +1,372 @@
import { shallow } from "enzyme";
import moment from "moment";
import React from "react";
import Product2 from "../../shared/enumerations/Product";
import {
ActivityState,
ActivityType,
IActivity,
SystemType
} from "../../shared/models/Activity";
import ActivityTransferToOwnDepartmentRequest from "./ActivityTransferToOwnDepartmentRequest";
import { IDepartmentAssignment } from "../../shared/models/DepartmentAssignment";
import { UserType } from "../../shared/models/Auditable";
describe("ActivityTransferToOwnDepartmentRequest", () => {
it("should call props onFormSubmit with selected department and current activity employeeId", () => {
const mockFormSubmit = jest.fn();
const mockOwnedDepartments: IDepartmentAssignment[] = [
{
id: 3,
employeeId: "C0000",
departmentId: 12,
startDate: moment(),
endDate: undefined,
note: "Test",
isHome: true,
slidingPayout: 1,
created: moment(),
createdBy: "George Frick",
createdByType: UserType.Associate,
lastUpdatedBy: "Incentive Manager",
lastUpdated: moment(),
lastUpdatedByType: UserType.System,
role: {
name: "test role",
rank: 1,
description: "test test",
hasCommissions: false,
permissions: [
{
code: "Secret",
description: "Secret Test Code"
}
]
}
},
{
id: 4,
employeeId: "C0000",
departmentId: 14,
startDate: moment(),
endDate: undefined,
note: "Test",
isHome: true,
slidingPayout: 1,
created: moment(),
createdBy: "George Frick",
createdByType: UserType.Associate,
lastUpdatedBy: "Incentive Manager",
lastUpdated: moment(),
lastUpdatedByType: UserType.System,
role: {
name: "test role",
rank: 1,
description: "test test",
hasCommissions: false,
permissions: [
{
code: "Secret",
description: "Secret Test Code"
}
]
}
}
];
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.Accepted
};
const wrapper = shallow(
<ActivityTransferToOwnDepartmentRequest
allDepartments={[]}
ownedDepartments={mockOwnedDepartments}
currentEmployeeId="C0000"
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(
wrapper
.find(
"[data-test='activity-transfer-request-own-department-button']"
)
.prop("disabled")
).toBe(true);
wrapper.find("[data-test='dept-input']").prop<any>("onChange")({
target: { value: "25" }
});
expect(
wrapper
.find(
"[data-test='activity-transfer-request-own-department-button']"
)
.prop("disabled")
).toBe(false);
wrapper
.find(
"[data-test='activity-transfer-request-own-department-button']"
)
.simulate("click");
expect(mockFormSubmit).toHaveBeenCalledTimes(1);
expect(mockFormSubmit).toHaveBeenLastCalledWith("25", "C0000");
});
it("it should not display if the activity is not owned by the current appUser", () => {
const mockFormSubmit = jest.fn();
const mockOwnedDepartments: IDepartmentAssignment[] = [
{
id: 3,
employeeId: "C0000",
departmentId: 12,
startDate: moment(),
endDate: undefined,
note: "Test",
isHome: true,
slidingPayout: 1,
created: moment(),
createdBy: "George Frick",
createdByType: UserType.Associate,
lastUpdatedBy: "Incentive Manager",
lastUpdated: moment(),
lastUpdatedByType: UserType.System,
role: {
name: "test role",
rank: 1,
description: "test test",
hasCommissions: false,
permissions: [
{
code: "Secret",
description: "Secret Test Code"
}
]
}
},
{
id: 4,
employeeId: "C0000",
departmentId: 14,
startDate: moment(),
endDate: undefined,
note: "Test",
isHome: true,
slidingPayout: 1,
created: moment(),
createdBy: "George Frick",
createdByType: UserType.Associate,
lastUpdatedBy: "Incentive Manager",
lastUpdated: moment(),
lastUpdatedByType: UserType.System,
role: {
name: "test role",
rank: 1,
description: "test test",
hasCommissions: false,
permissions: [
{
code: "Secret",
description: "Secret Test Code"
}
]
}
}
];
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.Accepted
};
const wrapper = shallow(
<ActivityTransferToOwnDepartmentRequest
allDepartments={[]}
ownedDepartments={mockOwnedDepartments}
currentEmployeeId="C0001"
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(
wrapper.find("[data-test='transfer-request-own-department-form']")
.length
).toBe(0);
});
it("it should not display if the appUser only has one current department", () => {
const mockFormSubmit = jest.fn();
const mockOwnedDepartments: IDepartmentAssignment[] = [
{
id: 3,
employeeId: "C0000",
departmentId: 12,
startDate: moment(),
endDate: undefined,
note: "Test",
isHome: true,
slidingPayout: 1,
created: moment(),
createdBy: "George Frick",
createdByType: UserType.Associate,
lastUpdatedBy: "Incentive Manager",
lastUpdated: moment(),
lastUpdatedByType: UserType.System,
role: {
name: "test role",
rank: 1,
description: "test test",
hasCommissions: false,
permissions: [
{
code: "Secret",
description: "Secret Test Code"
}
]
}
}
];
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.Accepted
};
const wrapper = shallow(
<ActivityTransferToOwnDepartmentRequest
allDepartments={[]}
ownedDepartments={mockOwnedDepartments}
currentEmployeeId="C0000"
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(
wrapper.find("[data-test='transfer-request-own-department-form']")
.length
).toBe(0);
});
it("should not display if the activity has payouts", () => {
const mockFormSubmit = jest.fn();
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [
{
payoutLineItem: {
id: 1,
activityPeriod: moment.utc(),
departmentId: 25,
role: {
description: "Teller",
name: "Teller Doe",
permissions: [],
hasCommissions: true,
rank: 1
},
product: "ATMCard",
goalId: 2,
goalValue: 3,
commissionId: 4,
commissionValue: 5,
employeeId: "123",
activityTotal: 100,
payoutTotal: 200,
slidingPayout: 12,
activityPayouts: undefined,
payoutProfileId: undefined
}
}
],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.Accepted
};
const wrapper = shallow(
<ActivityTransferToOwnDepartmentRequest
allDepartments={[]}
ownedDepartments={[]}
currentEmployeeId="C0000"
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(
wrapper.find("[data-test='transfer-request-own-department-form']")
.length
).toBe(0);
});
it("should not display if the activity is in PendingTransfer state", () => {
const mockFormSubmit = jest.fn();
const mockActivity: IActivity = {
id: 1,
product: Product2.ATMCard,
sourceSystem: SystemType.Dna,
activityPayouts: [],
departmentId: "1",
detail: {},
employeeEmail: "[email protected]",
employeeId: "C0000",
type: ActivityType.Sale,
state: ActivityState.PendingTransfer
};
const wrapper = shallow(
<ActivityTransferToOwnDepartmentRequest
allDepartments={[]}
ownedDepartments={[]}
currentEmployeeId="C0000"
activity={mockActivity}
onFormSubmit={mockFormSubmit}
/>
);
expect(
wrapper.find("[data-test='transfer-request-own-department-form']")
.length
).toBe(0);
});
});
@@ -0,0 +1,114 @@
import React, { useState } from "react";
import { Button, FormGroup, Input, Label } from "reactstrap";
import { IActivity } from "../../shared/models/Activity";
import { IDepartment } from "../../shared/models/Department";
import ActivityService from "../../shared/services/ActivityService";
import Translate from "../../translate/Translate";
import { IDepartmentAssignment } from "../../shared/models/DepartmentAssignment";
interface IActivityTransferRequestForm {
departmentId: string;
employeeId: string;
}
interface IActivityTransferToOwnDepartmentRequestProps {
ownedDepartments: IDepartmentAssignment[] | undefined;
allDepartments: IDepartment[];
activity: IActivity;
currentEmployeeId: string;
onFormSubmit: (departmentId: string, employeeId: string) => void;
}
const formTranslate =
Translate.Get.components.activityDetails
.activityTransferToOwnDepartmentRequest;
const ActivityTransferToOwnDepartmentRequest: React.FC<IActivityTransferToOwnDepartmentRequestProps> = (
props: IActivityTransferToOwnDepartmentRequestProps
) => {
const [formState, setFormState] = useState<IActivityTransferRequestForm>({
departmentId: "",
employeeId: props.activity.employeeId
});
const submitRequest = () => {
props.onFormSubmit(formState.departmentId, formState.employeeId);
};
function isActivityOwnedByUserWithMultipleDepartments(): boolean {
return (
props.ownedDepartments != null &&
props.ownedDepartments.length >= 2 &&
props.currentEmployeeId == props.activity.employeeId
);
}
// BUG? Did they tell you this is wrong and that it should work like approvals?
// OK. Just inject the activity.data.authorized from the parent and check if it can go to "PendingTransfer".
// For now they haven't asked for this, so still use the permission.
return ActivityService.isEligbleForTransfer(props.activity) &&
isActivityOwnedByUserWithMultipleDepartments() ? (
<div data-test="transfer-request-own-department-form">
<h5>{formTranslate.title}</h5>
<FormGroup>
<Label>{formTranslate.department.label}</Label>
<Input
bsSize="sm"
type="select"
data-test="dept-input"
value={formState.departmentId}
onChange={(event) => {
const departmentId = event.target.value;
setFormState((prevState) => ({
departmentId,
employeeId: prevState.employeeId
}));
}}
>
<option
data-test="placeholder-option"
hidden={formState.departmentId !== ""}
key=""
value=""
>
{formTranslate.department.placeholder}
</option>
{props.allDepartments.map((department) => {
if (
props.ownedDepartments?.some(
(ownedDepartment) =>
ownedDepartment.departmentId ===
department.id &&
department.id.toString() !=
props.activity.departmentId
)
)
return (
<option
key={department.id}
value={department.id.toString()}
>
{department.departmentName}
</option>
);
})}
</Input>
</FormGroup>
<Button
data-test="activity-transfer-request-own-department-button"
color="primary"
onClick={submitRequest}
disabled={
formState.employeeId.length === 0 ||
isNaN(parseInt(formState.departmentId))
}
>
{formTranslate.submitButton}
</Button>
</div>
) : (
<></>
);
};
export default ActivityTransferToOwnDepartmentRequest;
@@ -0,0 +1,140 @@
import { shallow } from "enzyme";
import React from "react";
import ActivityAuditLog from "./ActivityAuditLog";
import AsyncState from "./../../../shared/enumerations/AsyncState";
import { IActivitySearchForm } from "../../../shared/models/Activity";
describe("An associate opens the Activity Audit Log", () => {
let prevCalled = 0;
let nextCalled = 0;
const getPrev = () => {
prevCalled++;
};
const getNext = () => {
nextCalled++;
};
it("displays loading message when loading", () => {
let testCalls = 0;
let lastFilter: IActivitySearchForm = {
query: "",
dateHigh: null,
dateLow: null,
state: "",
product: "",
employeeId: "",
departmentId: 0
};
const testCall = (sort: string) => {
testCalls++;
};
let wrapper = shallow(
<ActivityAuditLog
activityContainer={{
state: AsyncState.Loading,
data: {
currentPage: 1,
items: [],
totalItems: 1,
totalPages: 1,
nextPage: "",
previousPage: ""
}
}}
getNextPage={getPrev}
getPrevPage={getNext}
form={lastFilter}
changeSort={testCall}
/>
);
expect(
wrapper.find("Alert").find('[color="warning"]').prop("hidden")
).toBe(false);
expect(
wrapper.find("Alert").find('[color="danger"]').prop("hidden")
).toBe(true);
});
it("displays loading message when error", () => {
let testCalls = 0;
let lastFilter: IActivitySearchForm = {
query: "",
dateHigh: null,
dateLow: null,
state: "",
product: "",
employeeId: "",
departmentId: 0
};
const testCall = (sort: string) => {
testCalls++;
};
let wrapper = shallow(
<ActivityAuditLog
activityContainer={{
state: AsyncState.Failure,
data: {
currentPage: 1,
items: [],
totalItems: 1,
totalPages: 1,
nextPage: "",
previousPage: ""
}
}}
getNextPage={getPrev}
getPrevPage={getNext}
form={lastFilter}
changeSort={testCall}
/>
);
expect(
wrapper.find("Alert").find('[color="warning"]').prop("hidden")
).toBe(true);
expect(
wrapper.find("Alert").find('[color="danger"]').prop("hidden")
).toBe(false);
});
it("displays no message when success", () => {
let testCalls = 0;
let lastFilter: IActivitySearchForm = {
query: "",
dateHigh: null,
dateLow: null,
state: "",
product: "",
employeeId: "",
departmentId: 0
};
const testCall = (sort: string) => {
testCalls++;
};
let wrapper = shallow(
<ActivityAuditLog
activityContainer={{
state: AsyncState.Success,
data: {
currentPage: 1,
items: [],
totalItems: 1,
totalPages: 1,
nextPage: "",
previousPage: ""
}
}}
getNextPage={getPrev}
getPrevPage={getNext}
form={lastFilter}
changeSort={testCall}
/>
);
expect(
wrapper.find("Alert").find('[color="warning"]').prop("hidden")
).toBe(true);
expect(
wrapper.find("Alert").find('[color="danger"]').prop("hidden")
).toBe(true);
});
});
@@ -0,0 +1,80 @@
import React from "react";
import { Alert, Col, Row } from "reactstrap";
import {
IActivitySearchForm,
IAuditableActivity
} from "../../../shared/models/Activity";
import Translate from "./../../../translate/Translate";
import IAsyncStateContainer from "../../../shared/types/IAsyncStateContainer";
import AsyncState from "../../../shared/enumerations/AsyncState";
import ActivityTable from "../activity-table/ActivityTable";
import IPagedResponse from "../../../shared/types/IPagedResponse";
import PagedContentControls from "../../paged-content-controls/PagedContentControls";
export interface IActivityAuditLogProps {
activityContainer: IAsyncStateContainer<IPagedResponse<IAuditableActivity>>;
getNextPage: () => void;
getPrevPage: () => void;
changeSort: (sort: string) => void;
form: IActivitySearchForm;
}
const ActivityAuditLogTranslate =
Translate.Get.components.activity.activityAuditLog;
const ActivityAuditLog: React.FC<IActivityAuditLogProps> = (props) => {
const changeSort = (headerName: string) => {
if (props.changeSort) {
props.changeSort(headerName);
}
};
return (
<div className="d-flex flex-row justify-content-center alight-items-center">
<div>
<Alert
color="warning"
hidden={
props.activityContainer.state !== AsyncState.Loading
}
>
<p className="m-0">
{ActivityAuditLogTranslate.loadingMessage}
</p>
</Alert>
<Alert
color="danger"
hidden={
props.activityContainer.state !== AsyncState.Failure
}
>
<p className="m-0">
{ActivityAuditLogTranslate.errorMessage}
</p>
</Alert>
<div className="d-flex flex-row justify-content-center align-items-center">
<PagedContentControls
nextPage={() => props.getNextPage()}
prevPage={() => props.getPrevPage()}
pagedResponse={props.activityContainer.data}
/>
</div>
<ActivityTable
activities={props.activityContainer.data?.items ?? []}
changeSort={changeSort}
sortBy={props.form.sortBy}
sortDescending={props.form.sortDescending}
/>
<div className="d-flex flex-row justify-content-center align-items-center">
<PagedContentControls
nextPage={() => props.getNextPage()}
prevPage={() => props.getPrevPage()}
pagedResponse={props.activityContainer.data}
/>
</div>
</div>
</div>
);
};
export default ActivityAuditLog;
@@ -0,0 +1,71 @@
import "./ActivityTable.scss";
import React from "react";
import { IAuditableActivity } from "./../../../shared/models/Activity";
import moment from "moment";
import Translate from "./../../../translate/Translate";
import { AutoTable } from "../../auto-table/AutoTable";
import Configuration from "../../../shared/configuration/Configuration";
import DepartmentDisplay from "../../../containers/shared/DepartmentDisplay";
import UserDisplay from "../../../containers/shared/UserDisplay";
import DateTimeService from "../../../shared/services/DateTimeService";
interface IActivityTable {
activities?: IAuditableActivity[];
}
const ActivityTableTranslate = Translate.Get.components.activity.activityTable;
const urlPrefix = Configuration.PrefixAppUrlWithBasename(
Configuration.Get.appUrls.activity
);
const ActivitySummaryTable: React.FC<IActivityTable> = (props) => {
const sortByUpdatedDate = (
data: IAuditableActivity[]
): IAuditableActivity[] => {
return data.sort((a, b) => moment(b.lastUpdated).diff(a.lastUpdated));
};
const headers = [
ActivityTableTranslate.headers.lastUpdated,
ActivityTableTranslate.headers.id,
ActivityTableTranslate.headers.createdBy,
ActivityTableTranslate.headers.sourceSystem,
ActivityTableTranslate.headers.detailAccountNumber,
ActivityTableTranslate.headers.detailSubjectPersonNbr,
ActivityTableTranslate.headers.detailMemberName
];
const cells = [
(x: IAuditableActivity) =>
DateTimeService.CastServerTimeToClientTimeString(x.lastUpdated),
(x: IAuditableActivity) => (
<a
data-test="activity-detail-link"
href={urlPrefix + "/" + x.id}
className="alert-link"
>
{x.id}
</a>
),
(x: IAuditableActivity) => x.createdBy,
(x: IAuditableActivity) => x.sourceSystem,
(x: IAuditableActivity) => x.detail?.accountNumber ?? "-",
(x: IAuditableActivity) => x.detail?.subjectPersonNbr ?? "-",
(x: IAuditableActivity) => x.detail?.memberName ?? "-"
];
if (props.activities && props.activities.length > 0) {
return (
<AutoTable
elements={sortByUpdatedDate(props.activities)}
headers={headers}
cells={cells}
/>
);
}
return <AutoTable elements={[]} headers={headers} cells={[]} />;
};
export default ActivitySummaryTable;

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