Created
February 13, 2024 04:22
-
-
Save jay-babu/feb3c93d34c50d67963c74c1afccc962 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/.github/ISSUE_TEMPLATE/pull_request_template.md b/.github/ISSUE_TEMPLATE/pull_request_template.md | |
new file mode 100644 | |
index 0000000..a4210a8 | |
--- /dev/null | |
+++ b/.github/ISSUE_TEMPLATE/pull_request_template.md | |
@@ -0,0 +1,32 @@ | |
+### All Submissions: | |
+ | |
+- [ ] Have you added authorization guards based on permission levels? | |
+- [ ] Is developer-facing telemtry added? | |
+ | |
+### New Feature Submissions: | |
+ | |
+- [ ] Describe how you tested this feature | |
+- [ ] Have you written tests? | |
+- [ ] Are customer-exposed audit logs added? | |
+ | |
+### Changes to Core Features: | |
+ | |
+- [ ] Have you written new tests for your core changes, as applicable? | |
+- [ ] Have you successfully ran tests with your changes locally? | |
+- [ ] Does this feature have any impact on transactions or the reporting of transactions? | |
+- [ ] If so, have you tested the following? | |
+ - [ ] Add item via barcode | |
+ - [ ] Add item via search | |
+ - [ ] Add item with qty multiplier | |
+ - [ ] Add item with keyboard hot keys | |
+ - [ ] Add item that does not exist and see that the modal cannot be closed | |
+ - [ ] Add item with parent item and verfify correct qty changing | |
+ - [ ] Add variant item | |
+ - [ ] Add tax except item | |
+ - [ ] Add dicounted item (during sale) | |
+ - [ ] Add promotional item | |
+ - [ ] Add multiple promotional items | |
+ - [ ] Add items from holds | |
+ - [ ] Checkout with credit | |
+ - [ ] Checkout with mutiple credit cards | |
+ | |
diff --git a/POSServiceModel b/POSServiceModel | |
index 38b2d15..af64a57 160000 | |
--- a/POSServiceModel | |
+++ b/POSServiceModel | |
@@ -1 +1 @@ | |
-Subproject commit 38b2d151cc6ddbbbaffa73403d818050d352f5ff | |
+Subproject commit af64a571f142ca2c2a33a1f31d520e4ca6d68e05 | |
diff --git a/package-lock.json b/package-lock.json | |
index e9c87c2..6259aeb 100644 | |
--- a/package-lock.json | |
+++ b/package-lock.json | |
@@ -42,12 +42,13 @@ | |
"react-apexcharts": "^1.4.1", | |
"react-dom": "^18.2.0", | |
"react-hook-form": "^7.47.0", | |
+ "react-hook-form-chakra": "^1.0.2", | |
"react-icons": "^4.12.0", | |
"react-infinite-scroll-component": "^6.1.0", | |
"react-json-view-lite": "^1.2.1", | |
"react-router-dom": "^6.14.0", | |
"react-scripts": "5.0.1", | |
- "swagger-typescript-api": "^12.0.4", | |
+ "swagger-typescript-api": "^13.0.3", | |
"typescript": "^4.9.5", | |
"use-debounce": "^10.0.0", | |
"use-query-params": "^2.2.1", | |
@@ -10233,6 +10234,17 @@ | |
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", | |
"integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" | |
}, | |
+ "node_modules/@sindresorhus/is": { | |
+ "version": "3.1.2", | |
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-3.1.2.tgz", | |
+ "integrity": "sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ==", | |
+ "engines": { | |
+ "node": ">=10" | |
+ }, | |
+ "funding": { | |
+ "url": "https://github.com/sindresorhus/is?sponsor=1" | |
+ } | |
+ }, | |
"node_modules/@sinonjs/commons": { | |
"version": "3.0.0", | |
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", | |
@@ -10250,30 +10262,41 @@ | |
} | |
}, | |
"node_modules/@stoplight/http-spec": { | |
- "version": "5.9.6", | |
- "resolved": "https://registry.npmjs.org/@stoplight/http-spec/-/http-spec-5.9.6.tgz", | |
- "integrity": "sha512-3BSNYLwUw/O8wXAeLalyNC6tMeDP7OffX3jiLBzxNKTqGiQJAbnRWdD6wcDqL2EtZLt6FBamHTI5vw9lNvUbew==", | |
+ "version": "7.0.2", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/http-spec/-/http-spec-7.0.2.tgz", | |
+ "integrity": "sha512-4DvT0w5goAhLxVbHfdzkMqGcTdi9bU4LmBrYNrZBOCFV4JPAHRERSBdI7F7n/MfgVvzxWb3Vftrh6pCgTd/+Jg==", | |
"dev": true, | |
"dependencies": { | |
"@stoplight/json": "^3.18.1", | |
"@stoplight/json-schema-generator": "1.0.2", | |
- "@stoplight/types": "^13.15.0", | |
+ "@stoplight/types": "14.1.0", | |
"@types/json-schema": "7.0.11", | |
"@types/swagger-schema-official": "~2.0.22", | |
"@types/type-is": "^1.6.3", | |
"fnv-plus": "^1.3.1", | |
- "lodash.isequalwith": "^4.4.0", | |
- "lodash.pick": "^4.4.0", | |
- "lodash.pickby": "^4.6.0", | |
+ "lodash": "^4.17.21", | |
"openapi3-ts": "^2.0.2", | |
"postman-collection": "^4.1.2", | |
- "tslib": "^2.3.1", | |
+ "tslib": "^2.6.2", | |
"type-is": "^1.6.18" | |
}, | |
"engines": { | |
"node": ">=14.13" | |
} | |
}, | |
+ "node_modules/@stoplight/http-spec/node_modules/@stoplight/types": { | |
+ "version": "14.1.0", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.0.tgz", | |
+ "integrity": "sha512-fL8Nzw03+diALw91xHEHA5Q0WCGeW9WpPgZQjodNUWogAgJ56aJs03P9YzsQ1J6fT7/XjDqHMgn7/RlsBzB/SQ==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "@types/json-schema": "^7.0.4", | |
+ "utility-types": "^3.10.0" | |
+ }, | |
+ "engines": { | |
+ "node": "^12.20 || >=14.13" | |
+ } | |
+ }, | |
"node_modules/@stoplight/http-spec/node_modules/@types/json-schema": { | |
"version": "7.0.11", | |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", | |
@@ -10331,9 +10354,9 @@ | |
} | |
}, | |
"node_modules/@stoplight/json-schema-ref-parser": { | |
- "version": "9.2.5", | |
- "resolved": "https://registry.npmjs.org/@stoplight/json-schema-ref-parser/-/json-schema-ref-parser-9.2.5.tgz", | |
- "integrity": "sha512-7UI3pX5oyGzAdGPah001CyPnIsJZJW+38sGjvx862zXQFidBe0sxFO5MUety61Zr/RaygCQ2RU/KfD7hSfOLxg==", | |
+ "version": "9.2.7", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/json-schema-ref-parser/-/json-schema-ref-parser-9.2.7.tgz", | |
+ "integrity": "sha512-1vNzJ7iSrFTAFNbZHPyhI6GiJJw74+WaV61bARUQEDR4Jm80f9s0Tq9uCvGoMYwIFmWDJAoTiyegnUs6SvVxDw==", | |
"dev": true, | |
"dependencies": { | |
"@jsdevtools/ono": "^7.1.3", | |
@@ -10346,19 +10369,32 @@ | |
} | |
}, | |
"node_modules/@stoplight/json-schema-sampler": { | |
- "version": "0.2.2", | |
- "resolved": "https://registry.npmjs.org/@stoplight/json-schema-sampler/-/json-schema-sampler-0.2.2.tgz", | |
- "integrity": "sha512-QP4ZwXh3dEn5wHZs2361kdf4BmaKiiP+pxIImAuVTLmulv9sBTB+ETG7Y5z9u4DOUQu2GNxfUY10iSwuBQMXrg==", | |
+ "version": "0.3.0", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/json-schema-sampler/-/json-schema-sampler-0.3.0.tgz", | |
+ "integrity": "sha512-G7QImi2xr9+8iPEg0D9YUi1BWhIiiEm19aMb91oWBSdxuhezOAqqRP3XNY6wczHV9jLWW18f+KkghTy9AG0BQA==", | |
"dev": true, | |
"dependencies": { | |
"@types/json-schema": "^7.0.7", | |
"json-pointer": "^0.6.1" | |
} | |
}, | |
+ "node_modules/@stoplight/json/node_modules/@stoplight/types": { | |
+ "version": "13.20.0", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", | |
+ "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "@types/json-schema": "^7.0.4", | |
+ "utility-types": "^3.10.0" | |
+ }, | |
+ "engines": { | |
+ "node": "^12.20 || >=14.13" | |
+ } | |
+ }, | |
"node_modules/@stoplight/ordered-object-literal": { | |
- "version": "1.0.4", | |
- "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.4.tgz", | |
- "integrity": "sha512-OF8uib1jjDs5/cCU+iOVy+GJjU3X7vk/qJIkIJFqwmlJKrrtijFmqwbu8XToXrwTYLQTP+Hebws5gtZEmk9jag==", | |
+ "version": "1.0.5", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.5.tgz", | |
+ "integrity": "sha512-COTiuCU5bgMUtbIFBuyyh2/yVVzlr5Om0v5utQDgBCuQUOPgU1DwoffkTfg4UBQOvByi5foF4w4T+H9CoRe5wg==", | |
"dev": true, | |
"engines": { | |
"node": ">=8" | |
@@ -10374,22 +10410,22 @@ | |
} | |
}, | |
"node_modules/@stoplight/prism-cli": { | |
- "version": "5.0.1", | |
- "resolved": "https://registry.npmjs.org/@stoplight/prism-cli/-/prism-cli-5.0.1.tgz", | |
- "integrity": "sha512-A13olRGUOeAxWWdv2lJ7JaufRmsvdVRNCYcyOjg17CTyIkZF46I0y3mvlHtICDDBH/85GjOk3OXI7a/UkQsSDA==", | |
+ "version": "5.5.4", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/prism-cli/-/prism-cli-5.5.4.tgz", | |
+ "integrity": "sha512-MvJUQcd8Fb3cuJuggjWinclz/JlHBeqZd5cYJtyJYs1Cz/woXjYF+0KeDviTdUTXEcTLhFguXS8vUK9vxqZ01g==", | |
"dev": true, | |
"dependencies": { | |
- "@stoplight/http-spec": "^5.9.2", | |
+ "@stoplight/http-spec": "^7.0.2", | |
"@stoplight/json": "^3.18.1", | |
- "@stoplight/json-schema-ref-parser": "9.2.5", | |
- "@stoplight/prism-core": "^5.0.1", | |
- "@stoplight/prism-http": "^5.0.1", | |
- "@stoplight/prism-http-server": "^5.0.1", | |
- "@stoplight/types": "^13.15.0", | |
+ "@stoplight/json-schema-ref-parser": "9.2.7", | |
+ "@stoplight/prism-core": "^5.5.4", | |
+ "@stoplight/prism-http": "^5.5.4", | |
+ "@stoplight/prism-http-server": "^5.5.4", | |
+ "@stoplight/types": "^14.1.0", | |
"chalk": "^4.1.2", | |
"chokidar": "^3.5.2", | |
"fp-ts": "^2.11.5", | |
- "json-schema-faker": "0.5.0-rcv.40", | |
+ "json-schema-faker": "0.5.3", | |
"lodash": "^4.17.21", | |
"node-fetch": "^2.6.5", | |
"pino": "^6.13.3", | |
@@ -10498,9 +10534,9 @@ | |
} | |
}, | |
"node_modules/@stoplight/prism-core": { | |
- "version": "5.0.1", | |
- "resolved": "https://registry.npmjs.org/@stoplight/prism-core/-/prism-core-5.0.1.tgz", | |
- "integrity": "sha512-rHdhmDrhBDg7yYipLJWlD/jRyECif5bAqDMVfobx1F6mzw+Yfc1YgXQbTgc+6oPwk8SgEr7+WQBq5jCi0ezHYw==", | |
+ "version": "5.5.4", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/prism-core/-/prism-core-5.5.4.tgz", | |
+ "integrity": "sha512-P/I1UD2HwP02EocTRFtjRJe3BeQbJASIGMbE1/rT5fHlYeOiMb+IerFfrTp+/AGO5a193UEDut3XBXR3dBuQcw==", | |
"dev": true, | |
"dependencies": { | |
"fp-ts": "^2.11.5", | |
@@ -10513,17 +10549,17 @@ | |
} | |
}, | |
"node_modules/@stoplight/prism-http": { | |
- "version": "5.0.1", | |
- "resolved": "https://registry.npmjs.org/@stoplight/prism-http/-/prism-http-5.0.1.tgz", | |
- "integrity": "sha512-/esXjNBbXjxZPtl3WyuvkgHUH7l3eDgOwIOP26qLH10BE3jlKR7IHF1gwZgEKdtBhQKQ0/2tB4fPJSAPaU2Blg==", | |
+ "version": "5.5.4", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/prism-http/-/prism-http-5.5.4.tgz", | |
+ "integrity": "sha512-W7NV3W09iAbAWj4ft6tqcBC2kdXpgArWSHO15glxQwRu1Y7I/U1Nr6EUhqQwyBgF5DWB5WFW/mnj30fZuyxGzw==", | |
"dev": true, | |
"dependencies": { | |
"@faker-js/faker": "^6.0.0", | |
"@stoplight/json": "^3.18.1", | |
"@stoplight/json-schema-merge-allof": "0.7.8", | |
- "@stoplight/json-schema-sampler": "0.2.2", | |
- "@stoplight/prism-core": "^5.0.1", | |
- "@stoplight/types": "^13.15.0", | |
+ "@stoplight/json-schema-sampler": "0.3.0", | |
+ "@stoplight/prism-core": "^5.5.4", | |
+ "@stoplight/types": "^14.1.0", | |
"@stoplight/yaml": "^4.2.3", | |
"abstract-logging": "^2.0.1", | |
"accepts": "^1.3.7", | |
@@ -10535,27 +10571,29 @@ | |
"fp-ts": "^2.11.5", | |
"http-proxy-agent": "^5.0.0", | |
"https-proxy-agent": "^5.0.0", | |
- "json-schema-faker": "0.5.0-rcv.40", | |
+ "json-schema-faker": "0.5.3", | |
"lodash": "^4.17.21", | |
"node-fetch": "^2.6.5", | |
+ "parse-multipart-data": "^1.5.0", | |
"pino": "^6.13.3", | |
"tslib": "^2.3.1", | |
"type-is": "^1.6.18", | |
- "uri-template-lite": "^22.9.0" | |
+ "uri-template-lite": "^22.9.0", | |
+ "whatwg-mimetype": "^3.0.0" | |
}, | |
"engines": { | |
"node": ">=16" | |
} | |
}, | |
"node_modules/@stoplight/prism-http-server": { | |
- "version": "5.0.1", | |
- "resolved": "https://registry.npmjs.org/@stoplight/prism-http-server/-/prism-http-server-5.0.1.tgz", | |
- "integrity": "sha512-VeDvw35JvEluyO1h5VEcxTlYwGLrk3GILLZNOB9oRoy31kwXyPrP4mdaFoAVyqfqa+wMkXmWn/6HdPDBsZTb+w==", | |
+ "version": "5.5.4", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/prism-http-server/-/prism-http-server-5.5.4.tgz", | |
+ "integrity": "sha512-2k/hWfxq+P7HQkoFpB9Bb18s+JDiiIZADXIQLYvk/bx6LO/cVxoZst9kqtM7UxugYrfkemYPP9/7/z0v73+ifg==", | |
"dev": true, | |
"dependencies": { | |
- "@stoplight/prism-core": "^5.0.1", | |
- "@stoplight/prism-http": "^5.0.1", | |
- "@stoplight/types": "^13.15.0", | |
+ "@stoplight/prism-core": "^5.5.4", | |
+ "@stoplight/prism-http": "^5.5.4", | |
+ "@stoplight/types": "^14.1.0", | |
"fast-xml-parser": "^4.2.0", | |
"fp-ts": "^2.11.5", | |
"io-ts": "^2.2.16", | |
@@ -10571,9 +10609,9 @@ | |
} | |
}, | |
"node_modules/@stoplight/prism-http-server/node_modules/node-fetch": { | |
- "version": "2.6.12", | |
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", | |
- "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", | |
+ "version": "2.7.0", | |
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | |
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | |
"dev": true, | |
"dependencies": { | |
"whatwg-url": "^5.0.0" | |
@@ -10694,9 +10732,9 @@ | |
"dev": true | |
}, | |
"node_modules/@stoplight/prism-http/node_modules/node-fetch": { | |
- "version": "2.6.12", | |
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", | |
- "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", | |
+ "version": "2.7.0", | |
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | |
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | |
"dev": true, | |
"dependencies": { | |
"whatwg-url": "^5.0.0" | |
@@ -10725,10 +10763,19 @@ | |
"node": ">=8" | |
} | |
}, | |
+ "node_modules/@stoplight/prism-http/node_modules/whatwg-mimetype": { | |
+ "version": "3.0.0", | |
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", | |
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", | |
+ "dev": true, | |
+ "engines": { | |
+ "node": ">=12" | |
+ } | |
+ }, | |
"node_modules/@stoplight/types": { | |
- "version": "13.15.0", | |
- "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.15.0.tgz", | |
- "integrity": "sha512-pBLjVRrWGVd+KzTbL3qrmufSKIEp0UfziDBdt/nrTHPKrlrtVwaHdrrQMcpM23yJDU1Wcg4cHvhIuGtKCT5OmA==", | |
+ "version": "14.1.1", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-14.1.1.tgz", | |
+ "integrity": "sha512-/kjtr+0t0tjKr+heVfviO9FrU/uGLc+QNX3fHJc19xsCNYqU7lVhaXxDmEID9BZTjG+/r9pK9xP/xU02XGg65g==", | |
"dev": true, | |
"dependencies": { | |
"@types/json-schema": "^7.0.4", | |
@@ -10759,6 +10806,19 @@ | |
"integrity": "sha512-sV+51I7WYnLJnKPn2EMWgS4EUfoP4iWEbrWwbXsj0MZCB/xOK8j6+C9fntIdOM50kpx45ZLC3s6kwKivWuqvyg==", | |
"dev": true | |
}, | |
+ "node_modules/@stoplight/yaml/node_modules/@stoplight/types": { | |
+ "version": "13.20.0", | |
+ "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-13.20.0.tgz", | |
+ "integrity": "sha512-2FNTv05If7ib79VPDA/r9eUet76jewXFH2y2K5vuge6SXbRHtWBhcaRmu+6QpF4/WRNoJj5XYRSwLGXDxysBGA==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "@types/json-schema": "^7.0.4", | |
+ "utility-types": "^3.10.0" | |
+ }, | |
+ "engines": { | |
+ "node": "^12.20 || >=14.13" | |
+ } | |
+ }, | |
"node_modules/@storybook/addon-actions": { | |
"version": "7.6.7", | |
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-7.6.7.tgz", | |
@@ -13012,14 +13072,14 @@ | |
} | |
}, | |
"node_modules/@storybook/preset-create-react-app": { | |
- "version": "7.6.7", | |
- "resolved": "https://registry.npmjs.org/@storybook/preset-create-react-app/-/preset-create-react-app-7.6.7.tgz", | |
- "integrity": "sha512-49m7yeyo1DiRoMqNk87UFg179C4+MYFPAy935K0WUwAlGKZ3/69ipYi8xYbtdAaBCXX5V86BI8HypEMLujWBVw==", | |
+ "version": "7.6.14", | |
+ "resolved": "https://registry.npmjs.org/@storybook/preset-create-react-app/-/preset-create-react-app-7.6.14.tgz", | |
+ "integrity": "sha512-XYSgBnLcLv/P+xV8QbIxgsaVF3BQwTXeEnkEjvU0Iyaiuw7HAPbFb3FRo+JSXU8QkFC3q5JTAUPtJ8KSKFEHsg==", | |
"dev": true, | |
"dependencies": { | |
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.1", | |
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", | |
- "@storybook/types": "7.6.7", | |
+ "@storybook/types": "7.6.14", | |
"@types/babel__core": "^7.1.7", | |
"@types/semver": "^7.3.4", | |
"pnp-webpack-plugin": "^1.7.0", | |
@@ -13034,6 +13094,66 @@ | |
"react-scripts": ">=5.0.0" | |
} | |
}, | |
+ "node_modules/@storybook/preset-create-react-app/node_modules/@storybook/channels": { | |
+ "version": "7.6.14", | |
+ "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.6.14.tgz", | |
+ "integrity": "sha512-tyrnnXTh7Ca6HbtzYtZGZmbUkC+eYPdot41+YDERMxXCnejd18BnsH/pyGW66GwgY079Q7uhdDFyM63ynZrt/A==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "@storybook/client-logger": "7.6.14", | |
+ "@storybook/core-events": "7.6.14", | |
+ "@storybook/global": "^5.0.0", | |
+ "qs": "^6.10.0", | |
+ "telejson": "^7.2.0", | |
+ "tiny-invariant": "^1.3.1" | |
+ }, | |
+ "funding": { | |
+ "type": "opencollective", | |
+ "url": "https://opencollective.com/storybook" | |
+ } | |
+ }, | |
+ "node_modules/@storybook/preset-create-react-app/node_modules/@storybook/client-logger": { | |
+ "version": "7.6.14", | |
+ "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.6.14.tgz", | |
+ "integrity": "sha512-rHa2hLU+80BN5E58Shf1g09YS6QEEOk5hwMuJ4WJfAypMDYPjnIsOYUboHClkCA9TDCH/iVhyRSPy83NWN2MZg==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "@storybook/global": "^5.0.0" | |
+ }, | |
+ "funding": { | |
+ "type": "opencollective", | |
+ "url": "https://opencollective.com/storybook" | |
+ } | |
+ }, | |
+ "node_modules/@storybook/preset-create-react-app/node_modules/@storybook/core-events": { | |
+ "version": "7.6.14", | |
+ "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.6.14.tgz", | |
+ "integrity": "sha512-zuSMjOgju7WLFL+okTXVvOKKNzwqVGRVp5UhXeSikT4aXuVdpfepCfikkjntn12G1ybL7mfFCsBU2DV1lwwp6Q==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "ts-dedent": "^2.0.0" | |
+ }, | |
+ "funding": { | |
+ "type": "opencollective", | |
+ "url": "https://opencollective.com/storybook" | |
+ } | |
+ }, | |
+ "node_modules/@storybook/preset-create-react-app/node_modules/@storybook/types": { | |
+ "version": "7.6.14", | |
+ "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.6.14.tgz", | |
+ "integrity": "sha512-sJ3qn45M2XLXlOi+wkhXK5xsXbSVzi8YGrusux//DttI3s8wCP3BQSnEgZkBiEktloxPferINHT1er8/9UK7Xw==", | |
+ "dev": true, | |
+ "dependencies": { | |
+ "@storybook/channels": "7.6.14", | |
+ "@types/babel__core": "^7.0.0", | |
+ "@types/express": "^4.7.0", | |
+ "file-system-cache": "2.3.0" | |
+ }, | |
+ "funding": { | |
+ "type": "opencollective", | |
+ "url": "https://opencollective.com/storybook" | |
+ } | |
+ }, | |
"node_modules/@storybook/preset-react-webpack": { | |
"version": "7.6.7", | |
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-7.6.7.tgz", | |
@@ -15074,9 +15194,9 @@ | |
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" | |
}, | |
"node_modules/@types/type-is": { | |
- "version": "1.6.3", | |
- "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", | |
- "integrity": "sha512-PNs5wHaNcBgCQG5nAeeZ7OvosrEsI9O4W2jAOO9BCCg4ux9ZZvH2+0iSCOIDBiKuQsiNS8CBlmfX9f5YBQ22cA==", | |
+ "version": "1.6.6", | |
+ "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", | |
+ "integrity": "sha512-fs1KHv/f9OvmTMsu4sBNaUu32oyda9Y9uK25naJG8gayxNrfqGIjPQsbLIYyfe7xFkppnPlJB+BuTldOaX9bXw==", | |
"dev": true, | |
"dependencies": { | |
"@types/node": "*" | |
@@ -16322,11 +16442,11 @@ | |
} | |
}, | |
"node_modules/axios": { | |
- "version": "1.5.0", | |
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", | |
- "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", | |
+ "version": "1.6.7", | |
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", | |
+ "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", | |
"dependencies": { | |
- "follow-redirects": "^1.15.0", | |
+ "follow-redirects": "^1.15.4", | |
"form-data": "^4.0.0", | |
"proxy-from-env": "^1.1.0" | |
} | |
@@ -18258,9 +18378,9 @@ | |
} | |
}, | |
"node_modules/cross-fetch/node_modules/node-fetch": { | |
- "version": "2.6.12", | |
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", | |
- "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", | |
+ "version": "2.7.0", | |
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | |
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | |
"dev": true, | |
"dependencies": { | |
"whatwg-url": "^5.0.0" | |
@@ -19512,6 +19632,11 @@ | |
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", | |
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" | |
}, | |
+ "node_modules/emojilib": { | |
+ "version": "2.4.0", | |
+ "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", | |
+ "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" | |
+ }, | |
"node_modules/emojis-list": { | |
"version": "3.0.0", | |
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", | |
@@ -21113,9 +21238,9 @@ | |
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" | |
}, | |
"node_modules/fast-redact": { | |
- "version": "3.2.0", | |
- "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.2.0.tgz", | |
- "integrity": "sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==", | |
+ "version": "3.3.0", | |
+ "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", | |
+ "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", | |
"dev": true, | |
"engines": { | |
"node": ">=6" | |
@@ -21491,9 +21616,9 @@ | |
} | |
}, | |
"node_modules/follow-redirects": { | |
- "version": "1.15.2", | |
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", | |
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", | |
+ "version": "1.15.5", | |
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", | |
+ "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", | |
"funding": [ | |
{ | |
"type": "individual", | |
@@ -21791,9 +21916,9 @@ | |
} | |
}, | |
"node_modules/fp-ts": { | |
- "version": "2.16.0", | |
- "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.0.tgz", | |
- "integrity": "sha512-bLq+KgbiXdTEoT1zcARrWEpa5z6A/8b7PcDW7Gef3NSisQ+VS7ll2Xbf1E+xsgik0rWub/8u0qP/iTTjj+PhxQ==", | |
+ "version": "2.16.2", | |
+ "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.2.tgz", | |
+ "integrity": "sha512-CkqAjnIKFqvo3sCyoBTqgJvF+bHrSik584S9nhTjtBESLx26cbtVMR/T9a6ApChOcSDAaM3JydDmWDUn4EEXng==", | |
"dev": true | |
}, | |
"node_modules/fraction.js": { | |
@@ -23004,9 +23129,9 @@ | |
} | |
}, | |
"node_modules/io-ts": { | |
- "version": "2.2.20", | |
- "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.20.tgz", | |
- "integrity": "sha512-Rq2BsYmtwS5vVttie4rqrOCIfHCS9TgpRLFpKQCM1wZBBRY9nWVGmEvm2FnDbSE2un1UE39DvFpTR5UL47YDcA==", | |
+ "version": "2.2.21", | |
+ "resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz", | |
+ "integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==", | |
"dev": true, | |
"peerDependencies": { | |
"fp-ts": "^2.5.0" | |
@@ -23615,9 +23740,9 @@ | |
} | |
}, | |
"node_modules/isomorphic-fetch/node_modules/node-fetch": { | |
- "version": "2.6.12", | |
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", | |
- "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", | |
+ "version": "2.7.0", | |
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", | |
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", | |
"dev": true, | |
"dependencies": { | |
"whatwg-url": "^5.0.0" | |
@@ -28218,16 +28343,16 @@ | |
} | |
}, | |
"node_modules/json-schema-faker": { | |
- "version": "0.5.0-rcv.40", | |
- "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rcv.40.tgz", | |
- "integrity": "sha512-BczZvu03jKrGh3ovCWrHusiX6MwiaKK2WZeyomKBNA8Nm/n7aBYz0mub1CnONB6cgxOZTNxx4afNmLblbUmZbA==", | |
+ "version": "0.5.3", | |
+ "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.3.tgz", | |
+ "integrity": "sha512-BeIrR0+YSrTbAR9dOMnjbFl1MvHyXnq+Wpdw1FpWZDHWKLzK229hZ5huyPcmzFUfVq1ODwf40WdGVoE266UBUg==", | |
"dev": true, | |
"dependencies": { | |
"json-schema-ref-parser": "^6.1.0", | |
- "jsonpath-plus": "^5.1.0" | |
+ "jsonpath-plus": "^7.2.0" | |
}, | |
"bin": { | |
- "jsf": "bin/gen.js" | |
+ "jsf": "bin/gen.cjs" | |
} | |
}, | |
"node_modules/json-schema-ref-parser": { | |
@@ -28281,12 +28406,12 @@ | |
} | |
}, | |
"node_modules/jsonpath-plus": { | |
- "version": "5.1.0", | |
- "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-5.1.0.tgz", | |
- "integrity": "sha512-890w2Pjtj0iswAxalRlt2kHthi6HKrXEfZcn+ZNZptv7F3rUGIeDuZo+C+h4vXBHLEsVjJrHeCm35nYeZLzSBQ==", | |
+ "version": "7.2.0", | |
+ "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", | |
+ "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", | |
"dev": true, | |
"engines": { | |
- "node": ">=10.0.0" | |
+ "node": ">=12.0.0" | |
} | |
}, | |
"node_modules/jsonpointer": { | |
@@ -28810,12 +28935,6 @@ | |
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", | |
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" | |
}, | |
- "node_modules/lodash.isequalwith": { | |
- "version": "4.4.0", | |
- "resolved": "https://registry.npmjs.org/lodash.isequalwith/-/lodash.isequalwith-4.4.0.tgz", | |
- "integrity": "sha512-dcZON0IalGBpRmJBmMkaoV7d3I80R2O+FrzsZyHdNSFrANq/cgDqKQNmAHE8UEj4+QYWwwhkQOVdLHiAopzlsQ==", | |
- "dev": true | |
- }, | |
"node_modules/lodash.memoize": { | |
"version": "4.1.2", | |
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", | |
@@ -28831,18 +28950,6 @@ | |
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", | |
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==" | |
}, | |
- "node_modules/lodash.pick": { | |
- "version": "4.4.0", | |
- "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", | |
- "integrity": "sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==", | |
- "dev": true | |
- }, | |
- "node_modules/lodash.pickby": { | |
- "version": "4.6.0", | |
- "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", | |
- "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", | |
- "dev": true | |
- }, | |
"node_modules/lodash.sortby": { | |
"version": "4.7.0", | |
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", | |
@@ -30642,11 +30749,14 @@ | |
} | |
}, | |
"node_modules/node-emoji": { | |
- "version": "1.11.0", | |
- "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", | |
- "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", | |
+ "version": "2.1.0", | |
+ "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.0.tgz", | |
+ "integrity": "sha512-tcsBm9C6FmPN5Wo7OjFi9lgMyJjvkAeirmjR/ax8Ttfqy4N8PoFic26uqFTIgayHPNI5FH4ltUvfh9kHzwcK9A==", | |
"dependencies": { | |
- "lodash": "^4.17.21" | |
+ "@sindresorhus/is": "^3.1.2", | |
+ "char-regex": "^1.0.2", | |
+ "emojilib": "^2.4.0", | |
+ "skin-tone": "^2.0.0" | |
} | |
}, | |
"node_modules/node-fetch": { | |
@@ -31631,6 +31741,12 @@ | |
"url": "https://github.com/sponsors/sindresorhus" | |
} | |
}, | |
+ "node_modules/parse-multipart-data": { | |
+ "version": "1.5.0", | |
+ "resolved": "https://registry.npmjs.org/parse-multipart-data/-/parse-multipart-data-1.5.0.tgz", | |
+ "integrity": "sha512-ck5zaMF0ydjGfejNMnlo5YU2oJ+pT+80Jb1y4ybanT27j+zbVP/jkYmCrUGsEln0Ox/hZmuvgy8Ra7AxbXP2Mw==", | |
+ "dev": true | |
+ }, | |
"node_modules/parse-prefer-header": { | |
"version": "1.0.0", | |
"resolved": "https://registry.npmjs.org/parse-prefer-header/-/parse-prefer-header-1.0.0.tgz", | |
@@ -33296,9 +33412,9 @@ | |
} | |
}, | |
"node_modules/postman-collection": { | |
- "version": "4.2.1", | |
- "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.2.1.tgz", | |
- "integrity": "sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA==", | |
+ "version": "4.3.0", | |
+ "resolved": "https://registry.npmjs.org/postman-collection/-/postman-collection-4.3.0.tgz", | |
+ "integrity": "sha512-QpmNOw1JhAVQTFWRz443/qpKs4/3T1MFrKqDZ84RS1akxOzhXXr15kD8+/+jeA877qyy9rfMsrFgLe2W7aCPjw==", | |
"dev": true, | |
"dependencies": { | |
"@faker-js/faker": "5.5.3", | |
@@ -33365,7 +33481,6 @@ | |
"version": "3.0.0", | |
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", | |
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", | |
- "dev": true, | |
"bin": { | |
"prettier": "bin/prettier.cjs" | |
}, | |
@@ -34197,6 +34312,16 @@ | |
"react": "^16.8.0 || ^17 || ^18" | |
} | |
}, | |
+ "node_modules/react-hook-form-chakra": { | |
+ "version": "1.0.2", | |
+ "resolved": "https://registry.npmjs.org/react-hook-form-chakra/-/react-hook-form-chakra-1.0.2.tgz", | |
+ "integrity": "sha512-QWoR9Sh5TXDPhPSX5mR4XZgdRXMBmH/R7iAqTTRRfOqsN3MM0oBvAHDxgPfhsJf68yrxTG3U5S1T3fsOrXeLdQ==", | |
+ "peerDependencies": { | |
+ "@chakra-ui/react": "^2", | |
+ "react": ">=17", | |
+ "react-hook-form": "^7" | |
+ } | |
+ }, | |
"node_modules/react-icons": { | |
"version": "4.12.0", | |
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", | |
@@ -36910,6 +37035,17 @@ | |
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", | |
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" | |
}, | |
+ "node_modules/skin-tone": { | |
+ "version": "2.0.0", | |
+ "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", | |
+ "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", | |
+ "dependencies": { | |
+ "unicode-emoji-modifier-base": "^1.0.0" | |
+ }, | |
+ "engines": { | |
+ "node": ">=8" | |
+ } | |
+ }, | |
"node_modules/slash": { | |
"version": "3.0.0", | |
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", | |
@@ -38099,24 +38235,24 @@ | |
"integrity": "sha512-rCC0NWGKr/IJhtRuPq/t37qvZHI/mH4I4sxflVM+qgVe5Z2uOCivzWaVbuioJaB61kvm5UvB7b49E+oBY0M8jA==" | |
}, | |
"node_modules/swagger-typescript-api": { | |
- "version": "12.0.4", | |
- "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-12.0.4.tgz", | |
- "integrity": "sha512-04ZxlJzu3g15TupfPhS0Yk0jzV/MM23WU4uuOl2vSi4yHrxEwnkIsoBkP084ec61q4vr2FHcI3DKxC+Mt1u10Q==", | |
+ "version": "13.0.3", | |
+ "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.0.3.tgz", | |
+ "integrity": "sha512-774ndLpGm2FNpUZpDugfoOO2pIcvSW9nlcqwLVSH9ju4YKCi1Gd83jPly7upcljOvZ8KO/edIUx+9eYViDYglg==", | |
"dependencies": { | |
"@types/swagger-schema-official": "2.0.22", | |
- "cosmiconfig": "7.0.1", | |
+ "cosmiconfig": "8.2.0", | |
"didyoumean": "^1.2.2", | |
- "eta": "^2.0.0", | |
+ "eta": "^2.2.0", | |
"js-yaml": "4.1.0", | |
"lodash": "4.17.21", | |
- "make-dir": "3.1.0", | |
- "nanoid": "3.3.4", | |
- "node-emoji": "1.11.0", | |
- "node-fetch": "^3.2.10", | |
- "prettier": "2.7.1", | |
+ "make-dir": "4.0.0", | |
+ "nanoid": "3.3.6", | |
+ "node-emoji": "2.1.0", | |
+ "node-fetch": "^3.3.1", | |
+ "prettier": "3.0.0", | |
"swagger-schema-official": "2.0.0-bab6bed", | |
"swagger2openapi": "7.0.8", | |
- "typescript": "4.8.4" | |
+ "typescript": "5.1.6" | |
}, | |
"bin": { | |
"sta": "index.js", | |
@@ -38129,18 +38265,20 @@ | |
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" | |
}, | |
"node_modules/swagger-typescript-api/node_modules/cosmiconfig": { | |
- "version": "7.0.1", | |
- "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", | |
- "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", | |
+ "version": "8.2.0", | |
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", | |
+ "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", | |
"dependencies": { | |
- "@types/parse-json": "^4.0.0", | |
"import-fresh": "^3.2.1", | |
+ "js-yaml": "^4.1.0", | |
"parse-json": "^5.0.0", | |
- "path-type": "^4.0.0", | |
- "yaml": "^1.10.0" | |
+ "path-type": "^4.0.0" | |
}, | |
"engines": { | |
- "node": ">=10" | |
+ "node": ">=14" | |
+ }, | |
+ "funding": { | |
+ "url": "https://github.com/sponsors/d-fischer" | |
} | |
}, | |
"node_modules/swagger-typescript-api/node_modules/js-yaml": { | |
@@ -38154,41 +38292,47 @@ | |
"js-yaml": "bin/js-yaml.js" | |
} | |
}, | |
- "node_modules/swagger-typescript-api/node_modules/nanoid": { | |
- "version": "3.3.4", | |
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", | |
- "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", | |
- "bin": { | |
- "nanoid": "bin/nanoid.cjs" | |
+ "node_modules/swagger-typescript-api/node_modules/make-dir": { | |
+ "version": "4.0.0", | |
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", | |
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", | |
+ "dependencies": { | |
+ "semver": "^7.5.3" | |
}, | |
"engines": { | |
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" | |
+ "node": ">=10" | |
+ }, | |
+ "funding": { | |
+ "url": "https://github.com/sponsors/sindresorhus" | |
} | |
}, | |
- "node_modules/swagger-typescript-api/node_modules/prettier": { | |
- "version": "2.7.1", | |
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", | |
- "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", | |
+ "node_modules/swagger-typescript-api/node_modules/nanoid": { | |
+ "version": "3.3.6", | |
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", | |
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", | |
+ "funding": [ | |
+ { | |
+ "type": "github", | |
+ "url": "https://github.com/sponsors/ai" | |
+ } | |
+ ], | |
"bin": { | |
- "prettier": "bin-prettier.js" | |
+ "nanoid": "bin/nanoid.cjs" | |
}, | |
"engines": { | |
- "node": ">=10.13.0" | |
- }, | |
- "funding": { | |
- "url": "https://github.com/prettier/prettier?sponsor=1" | |
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" | |
} | |
}, | |
"node_modules/swagger-typescript-api/node_modules/typescript": { | |
- "version": "4.8.4", | |
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", | |
- "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", | |
+ "version": "5.1.6", | |
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", | |
+ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", | |
"bin": { | |
"tsc": "bin/tsc", | |
"tsserver": "bin/tsserver" | |
}, | |
"engines": { | |
- "node": ">=4.2.0" | |
+ "node": ">=14.17" | |
} | |
}, | |
"node_modules/swagger2openapi": { | |
@@ -38863,9 +39007,9 @@ | |
} | |
}, | |
"node_modules/tslib": { | |
- "version": "2.5.3", | |
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", | |
- "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" | |
+ "version": "2.6.2", | |
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", | |
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" | |
}, | |
"node_modules/tsutils": { | |
"version": "3.21.0", | |
@@ -39052,6 +39196,14 @@ | |
"node": ">=4" | |
} | |
}, | |
+ "node_modules/unicode-emoji-modifier-base": { | |
+ "version": "1.0.0", | |
+ "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", | |
+ "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", | |
+ "engines": { | |
+ "node": ">=4" | |
+ } | |
+ }, | |
"node_modules/unicode-match-property-ecmascript": { | |
"version": "2.0.0", | |
"resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", | |
@@ -39530,9 +39682,9 @@ | |
"integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" | |
}, | |
"node_modules/utility-types": { | |
- "version": "3.10.0", | |
- "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", | |
- "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", | |
+ "version": "3.11.0", | |
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", | |
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", | |
"dev": true, | |
"engines": { | |
"node": ">= 4" | |
diff --git a/package.json b/package.json | |
index e998f17..2524421 100644 | |
--- a/package.json | |
+++ b/package.json | |
@@ -37,12 +37,13 @@ | |
"react-apexcharts": "^1.4.1", | |
"react-dom": "^18.2.0", | |
"react-hook-form": "^7.47.0", | |
+ "react-hook-form-chakra": "^1.0.2", | |
"react-icons": "^4.12.0", | |
"react-infinite-scroll-component": "^6.1.0", | |
"react-json-view-lite": "^1.2.1", | |
"react-router-dom": "^6.14.0", | |
"react-scripts": "5.0.1", | |
- "swagger-typescript-api": "^12.0.4", | |
+ "swagger-typescript-api": "^13.0.3", | |
"typescript": "^4.9.5", | |
"use-debounce": "^10.0.0", | |
"use-query-params": "^2.2.1", | |
diff --git a/src/App.tsx b/src/App.tsx | |
index 3fea800..a55a763 100644 | |
--- a/src/App.tsx | |
+++ b/src/App.tsx | |
@@ -1,4 +1,5 @@ | |
-import { lazy, Suspense } from "react"; | |
+import { useColorMode } from "@chakra-ui/react"; | |
+import { lazy, Suspense, useEffect } from "react"; | |
import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; | |
import { QueryParamProvider } from "use-query-params"; | |
import { ReactRouter6Adapter } from "use-query-params/adapters/react-router-6"; | |
@@ -395,6 +396,14 @@ const router = createBrowserRouter([ | |
]); | |
export const App = () => { | |
+ const colorMode = useColorMode(); | |
+ useEffect(() => { | |
+ if (colorMode.colorMode === "dark") { | |
+ document.body.classList.add("dark"); | |
+ } else { | |
+ document.body.classList.remove("dark"); | |
+ } | |
+ }, []); | |
return ( | |
<AuthContextProvider> | |
<AuthorizationContextProvider> | |
diff --git a/src/components/Auth/AuthenticationCard/AuthenticationEmailSignInForm.tsx b/src/components/Auth/AuthenticationCard/AuthenticationEmailSignInForm.tsx | |
index 2bead40..183531b 100644 | |
--- a/src/components/Auth/AuthenticationCard/AuthenticationEmailSignInForm.tsx | |
+++ b/src/components/Auth/AuthenticationCard/AuthenticationEmailSignInForm.tsx | |
@@ -40,6 +40,7 @@ export const AuthenticationEmailSignInForm: React.FC< | |
<FormLabel htmlFor="email">Email</FormLabel> | |
<Input | |
id="email" | |
+ name="email" | |
type="email" | |
onChange={(event) => { | |
setEmail(event.target.value); | |
diff --git a/src/components/Auth/AuthorizationFlowForms/SelectEmployeeForm.tsx b/src/components/Auth/AuthorizationFlowForms/SelectEmployeeForm.tsx | |
index 3c4de64..8914e90 100644 | |
--- a/src/components/Auth/AuthorizationFlowForms/SelectEmployeeForm.tsx | |
+++ b/src/components/Auth/AuthorizationFlowForms/SelectEmployeeForm.tsx | |
@@ -1,12 +1,20 @@ | |
import { ArrowBackIcon } from "@chakra-ui/icons"; | |
-import { Button, ButtonGroup, Progress, Text, VStack } from "@chakra-ui/react"; | |
+import { | |
+ Button, | |
+ ButtonGroup, | |
+ Center, | |
+ Spinner, | |
+ Text, | |
+ VStack, | |
+} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { motion } from "framer-motion"; | |
import { useCallback, useEffect, useState } from "react"; | |
import { BiRefresh } from "react-icons/bi"; | |
import { useLocalStorage } from "usehooks-ts"; | |
import { useEntityApi } from "../../../config/ItemsApi"; | |
import { useAuthorization } from "../../../context/AuthorizationContext/AuthorizationContext"; | |
-import { EntityDTO, UserDTO } from "../../../model/data-contracts"; | |
+import { EntityDTO, Role, UserDTO } from "../../../model/data-contracts"; | |
import { SelectableList } from "../../common/SelectableList.tsx/SelectableList"; | |
import { AvatarWithName } from "../AvatarWithName"; | |
@@ -33,11 +41,14 @@ export const SelectEmployeeForm: React.FC<SelectEmployeeListProps> = ({ | |
if (!entity) return; | |
setIsLoading(true); | |
try { | |
- const response = await entityApi?.getUsersByEntity(entity?.id); | |
+ const response = await entityApi?.getUsersByEntity(entity?.id, { | |
+ roles: [Role.OWNER, Role.ADMIN, Role.CASHIER, Role.MANAGER], | |
+ }); | |
if (response) { | |
return response.data; | |
} | |
} catch (error) { | |
+ Sentry.captureException(error); | |
console.log(error); | |
} finally { | |
setIsLoading(false); | |
@@ -69,7 +80,9 @@ export const SelectEmployeeForm: React.FC<SelectEmployeeListProps> = ({ | |
key={"selectEmployeeForm"} | |
> | |
{isLoading ? ( | |
- <Progress isIndeterminate /> | |
+ <Center> | |
+ <Spinner /> | |
+ </Center> | |
) : ( | |
<SelectableList | |
items={users.filter( | |
diff --git a/src/components/Auth/AuthorizationFlowForms/StoreCard.tsx b/src/components/Auth/AuthorizationFlowForms/StoreCard.tsx | |
index 258cec0..c606f89 100644 | |
--- a/src/components/Auth/AuthorizationFlowForms/StoreCard.tsx | |
+++ b/src/components/Auth/AuthorizationFlowForms/StoreCard.tsx | |
@@ -7,7 +7,11 @@ export type StoreCardProps = { | |
export const StoreCard = ({ entity }: StoreCardProps) => { | |
return ( | |
- <Card backgroundColor={"gray.50"} variant={"outline"}> | |
+ <Card | |
+ data-testid={`store-card-${entity.id}`} | |
+ backgroundColor={"gray.50"} | |
+ variant={"outline"} | |
+ > | |
<CardHeader> | |
<Heading size={"md"}>{entity.name}</Heading> | |
<Text>{entity.address}</Text> | |
diff --git a/src/components/Auth/AvatarWithName.tsx b/src/components/Auth/AvatarWithName.tsx | |
index 2093517..856c616 100644 | |
--- a/src/components/Auth/AvatarWithName.tsx | |
+++ b/src/components/Auth/AvatarWithName.tsx | |
@@ -31,7 +31,7 @@ export const AvatarWithName: React.FC<AvatarWithNameProps> = ({ | |
/> | |
<Divider orientation="vertical" /> | |
<Heading size={size}> | |
- {user.firstName} {user.lastName} | |
+ {user.firstName ? `${user.firstName} ${user.lastName}` : user.email} | |
</Heading> | |
</HStack> | |
); | |
diff --git a/src/components/Auth/OverrideButton.tsx b/src/components/Auth/OverrideButton.tsx | |
new file mode 100644 | |
index 0000000..8c1d999 | |
--- /dev/null | |
+++ b/src/components/Auth/OverrideButton.tsx | |
@@ -0,0 +1,19 @@ | |
+import { Button } from "@chakra-ui/react"; | |
+import { useEmployeeOverride } from "../../context/AuthorizationContext/EmployeeOverrideHandler"; | |
+ | |
+export type OverrideButtonProps = { | |
+ children?: React.ReactNode; | |
+}; | |
+ | |
+export const OverrideButton: React.FC<OverrideButtonProps> = ({ | |
+ children, | |
+ ...props | |
+}) => { | |
+ const { requestOverride } = useEmployeeOverride(); | |
+ | |
+ return ( | |
+ <Button colorScheme="red" onClick={() => requestOverride()} {...props}> | |
+ {children ?? "Override"} | |
+ </Button> | |
+ ); | |
+}; | |
diff --git a/src/components/Auth/PermissionedButton.tsx b/src/components/Auth/PermissionedButton.tsx | |
index a903eb9..18cdfa5 100644 | |
--- a/src/components/Auth/PermissionedButton.tsx | |
+++ b/src/components/Auth/PermissionedButton.tsx | |
@@ -8,7 +8,16 @@ export type PermissionedButtonProps = { | |
} & ButtonProps; | |
export const PermissionedButton = forwardRef( | |
- ({ requires, allowOverride, ...props }: PermissionedButtonProps, ref) => { | |
+ ( | |
+ { | |
+ requires, | |
+ allowOverride, | |
+ isDisabled, | |
+ children, | |
+ ...props | |
+ }: PermissionedButtonProps, | |
+ ref, | |
+ ) => { | |
const { hasPermission } = usePermissions(); | |
const isAllowed = Boolean(!requires || hasPermission(requires)); | |
@@ -20,12 +29,8 @@ export const PermissionedButton = forwardRef( | |
} | |
isDisabled={isAllowed} | |
> | |
- <Button | |
- {...props} | |
- isDisabled={!isAllowed || props.isDisabled} | |
- ref={ref} | |
- > | |
- {props.children} | |
+ <Button {...props} isDisabled={!isAllowed || isDisabled} ref={ref}> | |
+ {children} | |
</Button> | |
</Tooltip> | |
); | |
diff --git a/src/components/Auth/PermissionedIconButton.tsx b/src/components/Auth/PermissionedIconButton.tsx | |
new file mode 100644 | |
index 0000000..6c9e114 | |
--- /dev/null | |
+++ b/src/components/Auth/PermissionedIconButton.tsx | |
@@ -0,0 +1,38 @@ | |
+import { | |
+ forwardRef, | |
+ IconButton, | |
+ IconButtonProps, | |
+ Tooltip, | |
+} from "@chakra-ui/react"; | |
+import { usePermissions } from "../../context/PermissionsContext"; | |
+import { Permission } from "../../utils/Permission"; | |
+ | |
+export type PermissionedIconButtonProps = { | |
+ requires?: Permission | string; | |
+ allowOverride?: boolean; | |
+} & IconButtonProps; | |
+ | |
+export const PermissionedIconButton = forwardRef( | |
+ ({ requires, allowOverride, ...props }: PermissionedIconButtonProps, ref) => { | |
+ const { hasPermission } = usePermissions(); | |
+ | |
+ const isAllowed = Boolean(!requires || hasPermission(requires)); | |
+ | |
+ return ( | |
+ <Tooltip | |
+ label={ | |
+ allowOverride ? "Requires Override" : !isAllowed ? "Not allowed" : "" | |
+ } | |
+ isDisabled={isAllowed} | |
+ > | |
+ <IconButton | |
+ {...props} | |
+ isDisabled={!isAllowed || props.isDisabled} | |
+ ref={ref} | |
+ > | |
+ {props.children} | |
+ </IconButton> | |
+ </Tooltip> | |
+ ); | |
+ }, | |
+); | |
diff --git a/src/components/Auth/UnauthorizedCard.tsx b/src/components/Auth/UnauthorizedCard.tsx | |
index e042287..3402219 100644 | |
--- a/src/components/Auth/UnauthorizedCard.tsx | |
+++ b/src/components/Auth/UnauthorizedCard.tsx | |
@@ -1,6 +1,5 @@ | |
import { LockIcon } from "@chakra-ui/icons"; | |
import { | |
- Button, | |
ButtonGroup, | |
Card, | |
Center, | |
@@ -8,9 +7,9 @@ import { | |
Text, | |
VStack, | |
} from "@chakra-ui/react"; | |
-import { useNavigate } from "react-router-dom"; | |
import { useUserAuth } from "../../context/AuthenticationContext/AuthenticationContext"; | |
-import { useEmployeeOverride } from "../../context/AuthorizationContext/EmployeeOverrideHandler"; | |
+import { BackButton } from "../common/BackButton"; | |
+import { OverrideButton } from "./OverrideButton"; | |
export type UnauthorizedCardProps = { | |
overrideAllowed?: boolean; | |
@@ -25,12 +24,10 @@ export const UnauthorizedCard: React.FC<UnauthorizedCardProps> = ({ | |
message = "Please contact your administrator to request access.", | |
children, | |
}) => { | |
- const navigate = useNavigate(); | |
const { overriddenUser, user, storeUser } = useUserAuth(); | |
overrideAllowed = storeUser ? overrideAllowed : false; | |
- const { requestOverride } = useEmployeeOverride(); | |
return ( | |
<Card p={8}> | |
<Center> | |
@@ -40,16 +37,8 @@ export const UnauthorizedCard: React.FC<UnauthorizedCardProps> = ({ | |
<Text textAlign="center">{message}</Text> | |
{children ?? ( | |
<ButtonGroup w="100%" justifyContent="center"> | |
- {showBackButton && ( | |
- <Button variant="ghost" onClick={() => navigate(-1)}> | |
- Back | |
- </Button> | |
- )} | |
- {user && !overriddenUser && overrideAllowed && ( | |
- <Button colorScheme="red" onClick={() => requestOverride()}> | |
- Override | |
- </Button> | |
- )} | |
+ {showBackButton && <BackButton />} | |
+ {user && !overriddenUser && overrideAllowed && <OverrideButton />} | |
</ButtonGroup> | |
)} | |
</VStack> | |
diff --git a/src/components/CashDrawer.tsx b/src/components/CashDrawer.tsx | |
index 7be9f53..2992d5d 100644 | |
--- a/src/components/CashDrawer.tsx | |
+++ b/src/components/CashDrawer.tsx | |
@@ -1,3 +1,4 @@ | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useEffect, useState } from "react"; | |
const BAUD_RATE = 9600; | |
@@ -33,7 +34,9 @@ const useCashDrawer: () => [ | |
); | |
setDevice(deviceT); | |
}) | |
- .catch(() => undefined); | |
+ .catch((err) => { | |
+ Sentry.captureException(err); | |
+ }); | |
} | |
}, []); | |
diff --git a/src/components/DepartmentSummary.tsx b/src/components/DepartmentSummary.tsx | |
index cd65cff..83cf73a 100644 | |
--- a/src/components/DepartmentSummary.tsx | |
+++ b/src/components/DepartmentSummary.tsx | |
@@ -55,7 +55,7 @@ const DepartmentSummary = ({ | |
})} | |
<Tr> | |
<Td>Total</Td> | |
- <Td> | |
+ <Td data-testid="department-summary-total-amount"> | |
{USDollar.format( | |
departmentSummary.reduce( | |
(partialSum, summary) => partialSum + summary.totalPrice, | |
@@ -66,11 +66,15 @@ const DepartmentSummary = ({ | |
</Tr> | |
<Tr> | |
<Td>Total TAXABLE plus NON TAXABLE Sale</Td> | |
- <Td>{USDollar.format(taxNonTax)}</Td> | |
+ <Td data-testid="department-summary-total-taxable-non-taxable-sale"> | |
+ {USDollar.format(taxNonTax)} | |
+ </Td> | |
</Tr> | |
<Tr> | |
<Td>Total Bottle Deposit+Environment fees Sale</Td> | |
- <Td>{USDollar.format(itemFees)}</Td> | |
+ <Td data-testid="department-summary-total-bottle-deposit-environment-fees"> | |
+ {USDollar.format(itemFees)} | |
+ </Td> | |
</Tr> | |
</Tbody> | |
</Table> | |
diff --git a/src/components/Events/EventTypeTag.tsx b/src/components/Events/EventTypeTag.tsx | |
index a49d9b8..6ec1663 100644 | |
--- a/src/components/Events/EventTypeTag.tsx | |
+++ b/src/components/Events/EventTypeTag.tsx | |
@@ -4,7 +4,7 @@ export interface EventTypeTagProps { | |
type: string; | |
} | |
-const EventType = { | |
+export const EventType = { | |
ITEM_UPDATED: { | |
name: "Item Updated", | |
color: "blue", | |
diff --git a/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow1.tsx b/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow1.tsx | |
index 4dbf5b9..d80826f 100644 | |
--- a/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow1.tsx | |
+++ b/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow1.tsx | |
@@ -160,7 +160,6 @@ export const EditableItemRow1: React.FC<EditableItemRow1Props> = ({ | |
</Editable> | |
<ItemSizeUnitSelect | |
minW="70px" | |
- variant="flushed" | |
value={row.original.sizeUnit ?? ItemSizeUnit.ML} | |
onChange={(sizeUnit: ItemSizeUnit) => { | |
onItemDetailsChange({ | |
@@ -201,7 +200,7 @@ export const EditableItemRow1: React.FC<EditableItemRow1Props> = ({ | |
header: "Department", | |
cell: ({ getValue, row }) => ( | |
<Select | |
- variant={"flushed"} | |
+ w="200px" | |
placeholder={getValue().name} | |
onChange={(select) => { | |
onItemDetailsChange({ | |
diff --git a/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow3.tsx b/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow3.tsx | |
index 0656d44..e82066b 100644 | |
--- a/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow3.tsx | |
+++ b/src/components/ItemDetailComponents/EditableItemTable/EditableItemRow3.tsx | |
@@ -88,7 +88,7 @@ export const EditableItemRow3: React.FC<EditableItemRow3Props> = ({ | |
header: "Last Vendor", | |
cell: ({ getValue, row }) => ( | |
<Select | |
- variant={"flushed"} | |
+ w="200px" | |
placeholder={ | |
getValue() | |
? allVendors.find( | |
diff --git a/src/components/ItemSearchComponents/ItemSearchBox.tsx b/src/components/ItemSearchComponents/ItemSearchBox.tsx | |
index 5ca9c3e..931c1df 100644 | |
--- a/src/components/ItemSearchComponents/ItemSearchBox.tsx | |
+++ b/src/components/ItemSearchComponents/ItemSearchBox.tsx | |
@@ -81,10 +81,10 @@ const customComponents: Partial<SelectComponent> = { | |
...props | |
}: MultiValueGenericProps<ItemDetails>) => { | |
return ( | |
- <Tooltip label={itemLabelFormat(props.data)}> | |
+ <Tooltip label={itemLabelFormat(props.data.value)}> | |
<chakra.span display="inline-block" overflow="hidden"> | |
<chakraComponents.MultiValueLabel {...props}> | |
- {itemLabelFormat(props.data)} | |
+ {itemLabelFormat(props.data.value)} | |
</chakraComponents.MultiValueLabel> | |
</chakra.span> | |
</Tooltip> | |
@@ -118,7 +118,7 @@ const customComponents: Partial<SelectComponent> = { | |
const ItemSearchBox: React.FC<ItemSearchBoxProps> = ({ | |
isMulti, | |
placeholder, | |
- onSearchSelected, | |
+ onSearchSelected = () => undefined, | |
value, | |
persistSelection, | |
containerProps, | |
@@ -162,7 +162,7 @@ const ItemSearchBox: React.FC<ItemSearchBoxProps> = ({ | |
); | |
}); | |
return input; | |
- }, 500), | |
+ }, 300), | |
[api, entity?.id, filter], | |
); | |
@@ -175,7 +175,13 @@ const ItemSearchBox: React.FC<ItemSearchBoxProps> = ({ | |
menuPortalTarget={document.body} | |
useBasicStyles | |
autoFocus={autoFocus} | |
- value={value} | |
+ value={ | |
+ Array.isArray(value) | |
+ ? value.map((v) => ({ label: v.name, value: v })) | |
+ : value | |
+ ? { label: value.name, value } | |
+ : undefined | |
+ } | |
styles={{ | |
// container: (base) => ({ | |
// ...base, | |
@@ -200,11 +206,11 @@ const ItemSearchBox: React.FC<ItemSearchBoxProps> = ({ | |
closeMenuOnSelect={closeMenuOnSelect} | |
blurInputOnSelect={closeMenuOnSelect} //https://stackoverflow.com/questions/65036191/react-select-component-closemenuonselect-false-still-closing | |
isDisabled={isDisabled} | |
- inputValue={inputValue} | |
- onInputChange={(input, { action }) => { | |
- if (action !== "set-value") setInputValue(input); | |
- return input; | |
- }} | |
+ // inputValue={inputValue} | |
+ // onInputChange={(input, { action }) => { | |
+ // if (action !== "set-value") setInputValue(input); | |
+ // return input; | |
+ // }} | |
onChange={(val: SingleValue<any> | MultiValue<any>) => { | |
if (isMulti) { | |
if (val.length === 0) { | |
@@ -212,8 +218,8 @@ const ItemSearchBox: React.FC<ItemSearchBoxProps> = ({ | |
setSelected(""); | |
return; | |
} | |
+ // @ts-ignore | |
onSearchSelected?.([ | |
- ...(value ?? []), | |
...val.map((v: { value: ItemDetails }) => v.value), | |
]); | |
return; | |
@@ -222,6 +228,7 @@ const ItemSearchBox: React.FC<ItemSearchBoxProps> = ({ | |
setSelected(""); | |
return; | |
} | |
+ // @ts-ignore | |
onSearchSelected?.(val.value as ItemDetails); | |
setSelected(val.label); | |
}} | |
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx | |
index 05e3813..423636b 100644 | |
--- a/src/components/Loading.tsx | |
+++ b/src/components/Loading.tsx | |
@@ -1,11 +1,29 @@ | |
-import { Modal, ModalContent, Progress, Stack } from "@chakra-ui/react"; | |
+import { | |
+ Center, | |
+ Modal, | |
+ ModalContent, | |
+ Spinner, | |
+ Stack, | |
+ VStack, | |
+} from "@chakra-ui/react"; | |
+import { Logo } from "../Logo"; | |
const Loading = () => { | |
return ( | |
- <Stack background={"gray.100"} height={"100vh"} width={"100vw"}> | |
+ <Stack | |
+ key="loading" | |
+ background={"gray.100"} | |
+ height={"100vh"} | |
+ width={"100vw"} | |
+ > | |
<Modal size={"xl"} isOpen={true} onClose={() => 0}> | |
- <ModalContent> | |
- <Progress size="lg" isIndeterminate /> | |
+ <ModalContent m={0} bg="transparent" boxShadow="none" h="100%"> | |
+ <Center h="100%"> | |
+ <VStack w="100vw" justifyContent="center"> | |
+ <Logo width="50%" /> | |
+ <Spinner size="xl" /> | |
+ </VStack> | |
+ </Center> | |
</ModalContent> | |
</Modal> | |
</Stack> | |
diff --git a/src/components/MenuLinks.tsx b/src/components/MenuLinks.tsx | |
index 364bd52..1ff36d4 100644 | |
--- a/src/components/MenuLinks.tsx | |
+++ b/src/components/MenuLinks.tsx | |
@@ -116,6 +116,7 @@ const MenuLinks = () => { | |
}} | |
> | |
<MenuButton | |
+ data-testid="menu-button" | |
as={IconButton} | |
aria-label="Options" | |
icon={<HamburgerIcon />} | |
@@ -181,7 +182,7 @@ const MenuLinks = () => { | |
</MenuButton> | |
<MenuList> | |
<MenuGroup title="Transaction Reports"> | |
- <MenuItem as={Link} to="/transaction/by/date"> | |
+ <MenuItem as={Link} to="/transaction/by/date/"> | |
Transaction History | |
</MenuItem> | |
<MenuItem as={Link} to="/transaction/by/closing/"> | |
@@ -190,7 +191,7 @@ const MenuLinks = () => { | |
</MenuGroup> | |
<MenuDivider /> | |
<MenuGroup title="Item Reports"> | |
- <MenuItem as={Link} to="/item/sales/report"> | |
+ <MenuItem as={Link} to="/item/sales/report/"> | |
Item Sales | |
</MenuItem> | |
<MenuItem as={Link} to="/item/dead/stock/"> | |
@@ -217,7 +218,7 @@ const MenuLinks = () => { | |
> | |
Provi | |
</MenuItem> | |
- <MenuItem as={Link} to="/employee/time" icon={<TimeIcon />}> | |
+ <MenuItem as={Link} to="/employee/time/" icon={<TimeIcon />}> | |
Employee Time-clock | |
</MenuItem> | |
<MenuItem | |
diff --git a/src/components/PoleDisplay.tsx b/src/components/PoleDisplay.tsx | |
index 4bb4ae4..049779c 100644 | |
--- a/src/components/PoleDisplay.tsx | |
+++ b/src/components/PoleDisplay.tsx | |
@@ -1,3 +1,4 @@ | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useState } from "react"; | |
import { useEffectOnce } from "usehooks-ts"; | |
@@ -31,7 +32,9 @@ const usePoleDisplay = () => { | |
); | |
setDevice(deviceT); | |
}) | |
- .catch(() => undefined); | |
+ .catch((err) => { | |
+ Sentry.captureException(err); | |
+ }); | |
} | |
}); | |
@@ -41,7 +44,8 @@ const usePoleDisplay = () => { | |
try { | |
await device.open({ baudRate: BAUD_RATE }); | |
- } catch { | |
+ } catch (e) { | |
+ Sentry.captureException(e); | |
return; | |
} | |
const writer = device.writable.getWriter(); | |
diff --git a/src/components/QtyHotKeys.tsx b/src/components/QtyHotKeys.tsx | |
index 192a98b..3889306 100644 | |
--- a/src/components/QtyHotKeys.tsx | |
+++ b/src/components/QtyHotKeys.tsx | |
@@ -27,7 +27,7 @@ export const QtyHotKeys = ({ | |
return ( | |
<Flex justifyContent="flex-end"> | |
- <ButtonGroup gap="4"> | |
+ <ButtonGroup data-testid="qty-hot-keys" gap="4"> | |
{[2, 3, 4, 5, 6, 7, 8, 10, 12, 24].map((qty) => ( | |
<Button | |
key={qty} | |
diff --git a/src/components/SalesScreenComponents/BottomLine/BottomLine.tsx b/src/components/SalesScreenComponents/BottomLine/BottomLine.tsx | |
index bb6bf63..2d150fa 100644 | |
--- a/src/components/SalesScreenComponents/BottomLine/BottomLine.tsx | |
+++ b/src/components/SalesScreenComponents/BottomLine/BottomLine.tsx | |
@@ -11,8 +11,11 @@ import { BottomLineButtonGroup } from "./BottomLineButtonGroup"; | |
export interface BottomLineProps {} | |
export const BottomLine: React.FC<BottomLineProps> = ({}) => { | |
- const date = new Date(); | |
- date.setFullYear(date.getFullYear() - 21); // 21 years | |
+ const date = useMemo(() => { | |
+ const d = new Date(); | |
+ d.setFullYear(d.getFullYear() - 21); // 21 years | |
+ return d; | |
+ }, []); | |
const { | |
items: itemDetails, | |
transactionTotalPrice, | |
@@ -34,11 +37,13 @@ export const BottomLine: React.FC<BottomLineProps> = ({}) => { | |
isDisabled: false, | |
isTag: false, | |
onClick: () => driversLicenseScannerDisclosure.onOpen(), | |
+ "data-testid": "id-verification", | |
}, | |
{ | |
label: `Legal Age: ${date.toLocaleDateString()}`, | |
colorScheme: "red", | |
isTag: true, | |
+ "data-testid": "legal-age-tag", | |
}, | |
{ | |
label: `Total Quantity: ${[...itemDetails.values()].reduce( | |
@@ -48,16 +53,19 @@ export const BottomLine: React.FC<BottomLineProps> = ({}) => { | |
0, | |
)}`, | |
isTag: true, | |
+ "data-testid": "total-quantity-tag", | |
}, | |
{ | |
label: `Outstanding: ${USDollar.format( | |
transactionTotalPrice() - amountReceived.amountPaid, | |
)}`, | |
isTag: true, | |
+ "data-testid": "outstanding-tag", | |
}, | |
{ | |
label: `Total: ${USDollar.format(transactionTotalPrice())}`, | |
isTag: true, | |
+ "data-testid": "transaction-total-tag", | |
}, | |
], | |
[ | |
diff --git a/src/components/SalesScreenComponents/Modals/CartHoldsModal.tsx b/src/components/SalesScreenComponents/Modals/CartHoldsModal.tsx | |
index 05921a7..c6eb0bd 100644 | |
--- a/src/components/SalesScreenComponents/Modals/CartHoldsModal.tsx | |
+++ b/src/components/SalesScreenComponents/Modals/CartHoldsModal.tsx | |
@@ -21,7 +21,7 @@ import moment from "moment"; | |
import { useCallback } from "react"; | |
import { useForm } from "react-hook-form"; | |
import { useSalesContext } from "../../../context/Sales/SalesContext"; | |
-import { ItemWithQty } from "../../../pages/SalePage/SalePageUtils"; | |
+import { LineItem } from "../../../pages/SalePage/SalePageUtils"; | |
import { StyledInput } from "../../common/StyledInput"; | |
export type CartHoldsModalProps = { | |
@@ -30,7 +30,7 @@ export type CartHoldsModalProps = { | |
}; | |
export type Cart = { | |
- items: ItemWithQty[]; | |
+ items: LineItem[]; | |
taxExempt: boolean; | |
discount: number; | |
savedAt: Date; | |
@@ -42,11 +42,8 @@ export const CartHoldCard = (props: { | |
deleteCart: () => void; | |
closeModal: () => void; | |
}) => { | |
- const { | |
- setItems: setItemDetails, | |
- setDiscount, | |
- setTaxExempt, | |
- } = useSalesContext(); | |
+ const { calculateCartPrice, setItems, setDiscount, setTaxExempt } = | |
+ useSalesContext(); | |
return ( | |
<Tooltip | |
@@ -89,9 +86,11 @@ export const CartHoldCard = (props: { | |
</HStack> | |
<Button | |
onClick={() => { | |
- setItemDetails( | |
- new Map(props.cart.items.map((item) => [item.item.id, item])), | |
+ const newItemDetails = new Map( | |
+ props.cart.items.map((item) => [item.item.id, item]), | |
); | |
+ setItems(newItemDetails); | |
+ calculateCartPrice(newItemDetails); | |
setTaxExempt(props.cart.taxExempt); | |
setDiscount(props.cart.discount); | |
props.deleteCart(); | |
diff --git a/src/components/SalesScreenComponents/Modals/ChangeDueModal.tsx b/src/components/SalesScreenComponents/Modals/ChangeDueModal.tsx | |
index 9090a16..7794e0d 100644 | |
--- a/src/components/SalesScreenComponents/Modals/ChangeDueModal.tsx | |
+++ b/src/components/SalesScreenComponents/Modals/ChangeDueModal.tsx | |
@@ -52,6 +52,7 @@ export const ChangeDueModal: React.FC<ChangeDueModalProps> = ({ | |
<ModalFooter> | |
<Button | |
colorScheme="blue" | |
+ data-testid="paid" | |
mr={3} | |
isLoading={isPersisting} | |
isDisabled={isPersisting} | |
diff --git a/src/components/SalesScreenComponents/Modals/MultipleChoicesModal.tsx b/src/components/SalesScreenComponents/Modals/MultipleChoicesModal.tsx | |
index d497b8a..b07dd44 100644 | |
--- a/src/components/SalesScreenComponents/Modals/MultipleChoicesModal.tsx | |
+++ b/src/components/SalesScreenComponents/Modals/MultipleChoicesModal.tsx | |
@@ -11,26 +11,19 @@ import { | |
} from "@chakra-ui/react"; | |
import { useSalesContext } from "../../../context/Sales/SalesContext"; | |
import { ItemDetails } from "../../../model/data-contracts"; | |
-import { | |
- addSingleItem, | |
- ItemWithQty, | |
-} from "../../../pages/SalePage/SalePageUtils"; | |
-import usePoleDisplay from "../../PoleDisplay"; | |
export type MultipleChoicesModalProps = { | |
multipleChoicesModal: UseDisclosureReturn; | |
transactionTotalPrice: () => number; | |
- multipleChoices: ItemDetails[]; | |
+ multipleChoices: { items: ItemDetails[]; quantity: number }; | |
}; | |
export const MultipleChoicesModal: React.FC<MultipleChoicesModalProps> = ({ | |
multipleChoicesModal, | |
- transactionTotalPrice, | |
multipleChoices, | |
}) => { | |
const { isOpen, onClose } = multipleChoicesModal; | |
- const poleDisplay = usePoleDisplay(); | |
- const { quantity, taxExempt, setItems: setItemDetails } = useSalesContext(); | |
+ const { addSingleItemToCart } = useSalesContext(); | |
return ( | |
<Modal isOpen={isOpen} onClose={onClose}> | |
<ModalOverlay /> | |
@@ -39,7 +32,7 @@ export const MultipleChoicesModal: React.FC<MultipleChoicesModalProps> = ({ | |
<ModalCloseButton /> | |
<ModalBody> | |
<SimpleGrid columns={2} spacing="20px"> | |
- {multipleChoices?.map((choice) => { | |
+ {multipleChoices.items.map((choice) => { | |
return ( | |
<Button | |
key={choice.id} | |
@@ -47,27 +40,7 @@ export const MultipleChoicesModal: React.FC<MultipleChoicesModalProps> = ({ | |
overflowX={"hidden"} | |
variant="ghost" | |
onClick={() => { | |
- setItemDetails((prev) => { | |
- const m = new Map<number, ItemWithQty>(prev); | |
- const item = addSingleItem( | |
- choice, | |
- m, | |
- quantity, | |
- taxExempt, | |
- ); | |
- poleDisplay( | |
- `${item.quantity} ${item.item.name | |
- .trim() | |
- .substring(0, 9)} $${( | |
- item.item.sellPrice - item.discount | |
- ).toFixed(2)}`, | |
- `Grand Total $${( | |
- transactionTotalPrice() + item.totalPrice | |
- ).toFixed(2)}`, | |
- ); | |
- m.set(choice.id, item); | |
- return m; | |
- }); | |
+ addSingleItemToCart(choice, multipleChoices.quantity); | |
onClose(); | |
}} | |
> | |
diff --git a/src/components/SalesScreenComponents/Modals/PendingTransactionsModal.tsx b/src/components/SalesScreenComponents/Modals/PendingTransactionsModal.tsx | |
index 2be75ae..2dab402 100644 | |
--- a/src/components/SalesScreenComponents/Modals/PendingTransactionsModal.tsx | |
+++ b/src/components/SalesScreenComponents/Modals/PendingTransactionsModal.tsx | |
@@ -6,6 +6,7 @@ import { | |
ModalHeader, | |
ModalOverlay, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { createColumnHelper } from "@tanstack/react-table"; | |
import { useEffect, useMemo, useState } from "react"; | |
import { useTransactionsApi } from "../../../config/ItemsApi"; | |
@@ -58,17 +59,20 @@ export const PendingTransactionsModal: React.FC< | |
// effect to load data | |
useEffect(() => { | |
if (!entity || !transactionsApi || !isOpen) return; | |
- transactionsApi | |
- .getTransactions({ | |
- entityId: entity.id, | |
- transactionStatus: TransactionStatus.PENDING, | |
- }) | |
- .then((resp) => { | |
+ | |
+ const getPendingTransactions = async () => { | |
+ try { | |
+ const resp = await transactionsApi.getTransactions({ | |
+ entityId: entity.id, | |
+ transactionStatus: TransactionStatus.PENDING, | |
+ }); | |
setPendingTransactions(resp.data); | |
- }) | |
- .catch((err) => { | |
+ } catch (err) { | |
+ Sentry.captureException(err); | |
console.log(err); | |
- }); | |
+ } | |
+ }; | |
+ getPendingTransactions(); | |
}, [transactionsApi, entity, isOpen]); | |
return ( | |
diff --git a/src/components/SalesScreenComponents/Modals/QuickItemEditModal.tsx b/src/components/SalesScreenComponents/Modals/QuickItemEditModal.tsx | |
index 2fbc793..2fb5168 100644 | |
--- a/src/components/SalesScreenComponents/Modals/QuickItemEditModal.tsx | |
+++ b/src/components/SalesScreenComponents/Modals/QuickItemEditModal.tsx | |
@@ -13,7 +13,7 @@ import { | |
ModalOverlay, | |
useToast, | |
} from "@chakra-ui/react"; | |
-import { useCallback, useRef, useState } from "react"; | |
+import { useCallback, useEffect, useRef, useState } from "react"; | |
import { useItemsApi } from "../../../config/ItemsApi"; | |
import { ItemDetails } from "../../../model/data-contracts"; | |
import { PermissionedButton } from "../../Auth/PermissionedButton"; | |
@@ -34,9 +34,15 @@ export const QuickItemEditModal: React.FC<QuickItemEditModalProps> = ({ | |
}) => { | |
const itemsApi = useItemsApi(); | |
const toast = useToast(); | |
- const [itemDetails, setItemDetails] = useState<ItemDetails>(selectedItem); | |
+ const [itemDetails, setItemDetails] = useState<ItemDetails>(); | |
const saveRef = useRef<HTMLButtonElement>(null); | |
+ useEffect(() => { | |
+ itemsApi?.detailsDetail(selectedItem.id).then((res) => { | |
+ setItemDetails(res.data); | |
+ }); | |
+ }, [selectedItem.id, itemsApi]); | |
+ | |
const saveItem = useCallback( | |
(item: ItemDetails) => { | |
if (!itemsApi) return; | |
@@ -61,6 +67,22 @@ export const QuickItemEditModal: React.FC<QuickItemEditModalProps> = ({ | |
[toast, itemsApi, onUpdateSuccess, setItemDetails], | |
); | |
+ if (!itemDetails) | |
+ return ( | |
+ <Modal | |
+ isOpen={isOpen} | |
+ onClose={onClose} | |
+ size="8xl" | |
+ initialFocusRef={saveRef} | |
+ > | |
+ <ModalOverlay /> | |
+ <ModalContent> | |
+ <ModalHeader>Loading...</ModalHeader> | |
+ <ModalCloseButton /> | |
+ </ModalContent> | |
+ </Modal> | |
+ ); | |
+ | |
return ( | |
<Modal | |
isOpen={isOpen} | |
@@ -73,7 +95,12 @@ export const QuickItemEditModal: React.FC<QuickItemEditModalProps> = ({ | |
<ModalHeader> | |
<Editable | |
value={itemDetails.name} | |
- onChange={(name) => setItemDetails((prev) => ({ ...prev, name }))} | |
+ onChange={(name) => | |
+ setItemDetails((prev) => { | |
+ if (!prev) return prev; | |
+ return { ...prev, name }; | |
+ }) | |
+ } | |
> | |
<EditablePreview /> | |
<Input as={EditableInput} /> | |
diff --git a/src/components/SalesScreenComponents/SalesButtonGroups/CashOptions/CashOptions.tsx b/src/components/SalesScreenComponents/SalesButtonGroups/CashOptions/CashOptions.tsx | |
index 238f9d8..2bdbe0e 100644 | |
--- a/src/components/SalesScreenComponents/SalesButtonGroups/CashOptions/CashOptions.tsx | |
+++ b/src/components/SalesScreenComponents/SalesButtonGroups/CashOptions/CashOptions.tsx | |
@@ -20,9 +20,10 @@ export const CashOptions: React.FC<CashOptionsProps> = ({ | |
"$50", | |
`$${transactionTotalPrice().toFixed(2)}`, | |
`$${Math.ceil(transactionTotalPrice()).toFixed(2)}`, | |
- ].map((money) => { | |
+ ].map((money, i) => { | |
return { | |
isDisabled: !transactionItems.length || transactionTotalPrice() < 0, | |
+ "data-testid": `moneys-${i}`, | |
onClick: () => { | |
setAmountPaid((prev) => { | |
const newAmountPaid = prev.amountPaid + +money.replaceAll("$", ""); | |
diff --git a/src/components/SalesScreenComponents/SalesButtonGroups/Miscellaneous/Miscellaneous.tsx b/src/components/SalesScreenComponents/SalesButtonGroups/Miscellaneous/Miscellaneous.tsx | |
index 92d52ae..2eec148 100644 | |
--- a/src/components/SalesScreenComponents/SalesButtonGroups/Miscellaneous/Miscellaneous.tsx | |
+++ b/src/components/SalesScreenComponents/SalesButtonGroups/Miscellaneous/Miscellaneous.tsx | |
@@ -35,8 +35,10 @@ export const Miscellaneous: React.FC<MiscellaneousProps> = ({ deleteAll }) => { | |
() => [ | |
{ | |
label: "Discount All", | |
+ "data-testid": "discount-all", | |
leftIcon: <MdDiscount />, | |
isDisabled: !itemDetails.size, | |
+ disabled: !itemDetails.size, | |
onClick: () => { | |
onOpen(); | |
}, | |
@@ -45,8 +47,10 @@ export const Miscellaneous: React.FC<MiscellaneousProps> = ({ deleteAll }) => { | |
}, | |
{ | |
label: "Delete All", | |
+ "data-testid": "delete-all", | |
leftIcon: <DeleteIcon />, | |
isDisabled: !itemDetails.size, | |
+ disabled: !itemDetails.size, | |
onClick: () => { | |
deleteAll(); | |
}, | |
@@ -54,6 +58,7 @@ export const Miscellaneous: React.FC<MiscellaneousProps> = ({ deleteAll }) => { | |
}, | |
{ | |
label: "Holds", | |
+ "data-testid": "holds", | |
leftIcon: <FaShoppingCart />, | |
onClick: cartHoldsDisclosure.onOpen, | |
}, | |
@@ -68,6 +73,7 @@ export const Miscellaneous: React.FC<MiscellaneousProps> = ({ deleteAll }) => { | |
e.currentTarget.blur(); | |
setTaxExempt(e.currentTarget.checked); | |
}} | |
+ data-testid="tax-exempt-switch" | |
/> | |
</FormControl>, | |
], | |
diff --git a/src/components/SalesScreenComponents/SalesButtonGroups/QuickPicks/QuickPicks.tsx b/src/components/SalesScreenComponents/SalesButtonGroups/QuickPicks/QuickPicks.tsx | |
index ecb7772..c3ae2bc 100644 | |
--- a/src/components/SalesScreenComponents/SalesButtonGroups/QuickPicks/QuickPicks.tsx | |
+++ b/src/components/SalesScreenComponents/SalesButtonGroups/QuickPicks/QuickPicks.tsx | |
@@ -12,17 +12,16 @@ export interface QuickPicksProps { | |
retrieveItemsLikeUpc: (upc: string) => void; | |
} | |
+export const NEW_ITEM_UPC = "NEW ITEM"; | |
+ | |
export const QuickPicks: React.FC<QuickPicksProps> = ({ | |
retrieveItemsLikeUpc, | |
}) => { | |
- const { | |
- setItems: setItemDetails, | |
- taxExempt, | |
- transactionTotalPrice, | |
- } = useSalesContext(); | |
+ const { calculateCartPrice, setItems, taxExempt, transactionTotalPrice } = | |
+ useSalesContext(); | |
const { isOpen, onOpen, onClose } = useDisclosure(); | |
const poleDisplay = usePoleDisplay(); | |
- const [entity] = useEntitySelected(); | |
+ const [entity, , isEntityLoading] = useEntitySelected(); | |
const QUICK_PICKS = useMemo(() => { | |
if (!entity?.id) return []; | |
@@ -53,12 +52,12 @@ export const QuickPicks: React.FC<QuickPicksProps> = ({ | |
}, | |
<NewItem | |
onSubmit={(price, department, bottleFee, envFee) => | |
- setItemDetails((prev) => { | |
+ setItems((prev) => { | |
const m = new Map(prev); | |
- const item = addSingleItem( | |
+ const lineItem = addSingleItem( | |
{ | |
id: Math.floor(Math.random() * 900000), // any random id is fine as new item don't have ids when saved | |
- upc: "NEW ITEM", | |
+ upc: NEW_ITEM_UPC, | |
onHandQuantity: 1, | |
name: "UNKNOWN ITEM", | |
sellPrice: price, | |
@@ -76,14 +75,15 @@ export const QuickPicks: React.FC<QuickPicksProps> = ({ | |
taxExempt, | |
); | |
poleDisplay( | |
- `${item.quantity} ${item.item.name.trim().substring(0, 9)} $${( | |
- item.item.sellPrice - item.discount | |
- ).toFixed(2)}`, | |
+ `${lineItem.quantity} ${lineItem.item.name | |
+ .trim() | |
+ .substring(0, 9)} $${lineItem.item.sellPrice.toFixed(2)}`, | |
`Grand Total $${( | |
- transactionTotalPrice() + item.totalPrice | |
+ transactionTotalPrice() + lineItem.totalPrice | |
).toFixed(2)}`, | |
); | |
- m.set(item.item.id, item); | |
+ m.set(lineItem.item.id, lineItem); | |
+ calculateCartPrice(m); | |
return m; | |
}) | |
} | |
@@ -96,14 +96,17 @@ export const QuickPicks: React.FC<QuickPicksProps> = ({ | |
// }, | |
]; | |
}, [ | |
+ calculateCartPrice, | |
+ QUICK_PICKS, | |
retrieveItemsLikeUpc, | |
- setItemDetails, | |
+ setItems, | |
taxExempt, | |
transactionTotalPrice, | |
poleDisplay, | |
- onOpen, | |
]); | |
+ if (isEntityLoading) return null; | |
+ | |
return ( | |
<> | |
<SalesButtonGroup colorScheme="yellow" elements={elements} /> | |
diff --git a/src/components/SalesScreenComponents/SalesScreenItemsTable.tsx b/src/components/SalesScreenComponents/SalesScreenItemsTable.tsx | |
index 17fac3b..62efc34 100644 | |
--- a/src/components/SalesScreenComponents/SalesScreenItemsTable.tsx | |
+++ b/src/components/SalesScreenComponents/SalesScreenItemsTable.tsx | |
@@ -1,5 +1,6 @@ | |
import { DeleteIcon } from "@chakra-ui/icons"; | |
-import { HStack, Icon, IconButton, Text } from "@chakra-ui/react"; | |
+import { HStack, Icon, IconButton, Text, Tooltip } from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { createColumnHelper } from "@tanstack/react-table"; | |
import { | |
Dispatch, | |
@@ -14,25 +15,28 @@ import { useItemsApi, useTransactionApi } from "../../config/ItemsApi"; | |
import { useEntitySelected } from "../../context/EntityProvider"; | |
import { usePermissions } from "../../context/PermissionsContext"; | |
import { useRegisterProvider } from "../../context/RegisterProvider"; | |
-import { useSalesContext } from "../../context/Sales/SalesContext"; | |
-import { Item, ItemDetails } from "../../model/data-contracts"; | |
import { | |
- calculateTotalPrice, | |
- ItemWithQty, | |
-} from "../../pages/SalePage/SalePageUtils"; | |
+ SalesContextType, | |
+ useSalesContext, | |
+} from "../../context/Sales/SalesContext"; | |
+import { Item, ItemDetails } from "../../model/data-contracts"; | |
+import { USDollar } from "../../pages/SalePage/SalePage"; | |
+import { LineItem } from "../../pages/SalePage/SalePageUtils"; | |
import { NumberInputWithSideSteppers } from "../common/NumberInputWithSideSteppers"; | |
import usePoleDisplay from "../PoleDisplay"; | |
import { CurrencyRow } from "../Table/CurrencyRow"; | |
import { TransformityTable } from "../Table/TransformityTable"; | |
import { QuickItemEditModal } from "./Modals/QuickItemEditModal"; | |
+import { NEW_ITEM_UPC } from "./SalesButtonGroups/QuickPicks/QuickPicks"; | |
export interface SalesScreenItemsTableProps { | |
transactionTotalPrice: () => number; | |
} | |
-type ItemWithQtyProps = ItemWithQty & { | |
+type ItemWithQtyProps = LineItem & { | |
poleDisplay: ReturnType<typeof usePoleDisplay>; | |
- setItemDetails: Dispatch<SetStateAction<Map<number, ItemWithQty>>>; | |
+ setItemDetails: Dispatch<SetStateAction<Map<number, LineItem>>>; | |
+ calculateCartPrice: SalesContextType["calculateCartPrice"]; | |
transactionTotalPrice: () => number; | |
removeItem: (id: number) => void; | |
taxExempt: boolean; | |
@@ -52,20 +56,23 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
items: itemDetails, | |
setItems: setItemDetails, | |
taxExempt, | |
+ calculateCartPrice, | |
} = useSalesContext(); | |
const poleDisplay = usePoleDisplay(); | |
const [entity] = useEntitySelected(); | |
const { register } = useRegisterProvider(); | |
const removeItem = useCallback( | |
- (id: number) => { | |
+ async (id: number) => { | |
if (!entity?.id) return; | |
- transactionApi | |
- ?.emptyCart({ | |
+ try { | |
+ await transactionApi?.emptyCart({ | |
entityId: entity?.id, | |
registerNumber: register.registerNumber, | |
itemId: id, | |
- }) | |
- .catch((err) => console.error(err)); | |
+ }); | |
+ } catch (err) { | |
+ Sentry.captureException(err); | |
+ } | |
setItemDetails((prev) => { | |
const m = new Map(prev); | |
const item = m.get(id); | |
@@ -78,6 +85,7 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
)}`, | |
); | |
} | |
+ calculateCartPrice(m); | |
return m; | |
}); | |
}, | |
@@ -88,6 +96,7 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
setItemDetails, | |
transactionApi, | |
transactionTotalPrice, | |
+ calculateCartPrice, | |
], | |
); | |
@@ -103,16 +112,20 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
const columns: any[] = useMemo( | |
() => [ | |
- columnHelper.accessor((_row: ItemWithQty, i: number) => i + 1, { | |
+ columnHelper.accessor((_row: LineItem, i: number) => i + 1, { | |
header: "#", | |
}), | |
columnHelper.accessor("item.name", { | |
header: "Name", | |
cell: ({ getValue, row }) => ( | |
<HStack | |
- cursor={"pointer"} | |
+ cursor={ | |
+ row.original.item.upc !== NEW_ITEM_UPC ? "pointer" : undefined | |
+ } | |
onClick={() => | |
- hasPermission("item/*:read") && setSelectedItem(row.original.item) | |
+ hasPermission("item/*:read") && | |
+ row.original.item.upc !== NEW_ITEM_UPC && | |
+ setSelectedItem(row.original.item) | |
} | |
> | |
<Text>{getValue()}</Text> | |
@@ -122,99 +135,125 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
}), | |
columnHelper.accessor("item.sellPrice", { | |
header: "Price", | |
- cell: ({ getValue }) => ( | |
- <CurrencyRow | |
- value={getValue()} | |
- containerProps={{ textAlign: undefined }} | |
- /> | |
- ), | |
- }), | |
- columnHelper.accessor("discount", { | |
- header: "Discount", | |
- cell: ({ getValue }) => ( | |
+ cell: ({ getValue, row }) => ( | |
<CurrencyRow | |
+ data-testid={`price-${row.index}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ maxW={"fit-content"} | |
value={getValue()} | |
- containerProps={{ textAlign: undefined }} | |
+ textAlign={undefined} | |
/> | |
), | |
}), | |
columnHelper.accessor("quantity", { | |
header: "Quantity", | |
- cell: ({ getValue, row: { original: item } }) => ( | |
+ cell: ({ getValue, row: { original: lineItem } }) => ( | |
<NumberInputWithSideSteppers | |
- key={item.item.id} | |
+ key={lineItem.item.id} | |
maxWidth={"190px"} | |
precision={0} | |
step={1} | |
value={getValue()} | |
onChange={(val) => { | |
- item.quantityAsString = val.toString(); | |
+ lineItem.quantityAsString = val.toString(); | |
try { | |
- item.quantity = val; | |
- item.setItemDetails((prev) => { | |
+ lineItem.quantity = val; | |
+ lineItem.setItemDetails((prev) => { | |
const m = new Map(prev); | |
- item.totalPrice = calculateTotalPrice(item, item.taxExempt); | |
- m.set(item.item.id, item); | |
+ | |
+ m.set(lineItem.item.id, lineItem); | |
+ lineItem.calculateCartPrice(m); | |
return m; | |
}); | |
- item.poleDisplay( | |
- `${item.quantity} ${item.item.name | |
+ lineItem.poleDisplay( | |
+ `${lineItem.quantity} ${lineItem.item.name | |
.trim() | |
- .substring(0, 9)} ${( | |
- item.item.sellPrice - item.discount | |
- ).toFixed(2)}`, | |
- `${item.transactionTotalPrice().toFixed(2)}`, | |
+ .substring(0, 9)} ${lineItem.item.sellPrice.toFixed(2)}`, | |
+ `${lineItem.transactionTotalPrice().toFixed(2)}`, | |
); | |
- } catch { | |
+ } catch (err) { | |
// Do nothing | |
+ Sentry.captureException(err); | |
} | |
}} | |
/> | |
), | |
}), | |
- columnHelper.accessor( | |
- (row: ItemWithQty) => | |
- (row.item.sellPrice - row.discount) * row.quantity, | |
- { | |
- header: "Sub-total", | |
- cell: ({ getValue }) => ( | |
- <CurrencyRow | |
- value={getValue()} | |
- containerProps={{ textAlign: undefined }} | |
- /> | |
- ), | |
- }, | |
- ), | |
- columnHelper.accessor( | |
- (row: ItemWithQty) => row.item.department.environmentFee * row.quantity, | |
- { | |
- header: "Env Fee", | |
- cell: ({ getValue }) => ( | |
- <CurrencyRow | |
- value={getValue()} | |
- containerProps={{ textAlign: undefined }} | |
- /> | |
- ), | |
- }, | |
- ), | |
- columnHelper.accessor( | |
- (row: ItemWithQty) => row.item.department.bottleDeposit * row.quantity, | |
- { | |
- header: "Deposit", | |
- cell: ({ getValue }) => ( | |
- <CurrencyRow | |
- value={getValue()} | |
- containerProps={{ textAlign: undefined }} | |
- /> | |
- ), | |
+ columnHelper.accessor("discount", { | |
+ header: "DISCOUNT", | |
+ cell: ({ getValue, row }) => { | |
+ return ( | |
+ <Tooltip | |
+ label={row.original.promotionAmounts?.map((p) => ( | |
+ <p> | |
+ Promotion: {p.promotionName} - {USDollar.format(p.amount)} | |
+ </p> | |
+ ))} | |
+ isDisabled={row.original.promotionAmounts?.length === 0} | |
+ hasArrow | |
+ > | |
+ <CurrencyRow | |
+ data-testid={`discount-${row.index}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ maxW={"fit-content"} | |
+ value={getValue()} | |
+ textAlign={undefined} | |
+ /> | |
+ </Tooltip> | |
+ ); | |
}, | |
- ), | |
+ }), | |
+ columnHelper.accessor((row: LineItem) => row.subtotal, { | |
+ header: "Sub-total", | |
+ cell: ({ getValue, row }) => ( | |
+ <CurrencyRow | |
+ data-testid={`subtotal-${row.index}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ maxW={"fit-content"} | |
+ value={getValue()} | |
+ textAlign={undefined} | |
+ /> | |
+ ), | |
+ }), | |
+ columnHelper.accessor((row: LineItem) => row.environmentFee, { | |
+ header: "Env Fee", | |
+ cell: ({ getValue, row }) => ( | |
+ <CurrencyRow | |
+ data-testid={`env-fee-${row.index}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ maxW={"fit-content"} | |
+ value={getValue()} | |
+ textAlign={undefined} | |
+ /> | |
+ ), | |
+ }), | |
+ columnHelper.accessor((row: LineItem) => row.bottleDeposit, { | |
+ header: "Deposit", | |
+ cell: ({ getValue, row }) => ( | |
+ <CurrencyRow | |
+ data-testid={`deposit-${row.index}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ maxW={"fit-content"} | |
+ value={getValue()} | |
+ textAlign={undefined} | |
+ /> | |
+ ), | |
+ }), | |
columnHelper.accessor("totalPrice", { | |
header: "Item Total", | |
- cell: ({ getValue }) => ( | |
+ cell: ({ getValue, row }) => ( | |
<CurrencyRow | |
+ data-testid={`total-price-${row.index}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ maxW={"fit-content"} | |
value={getValue()} | |
- containerProps={{ textAlign: undefined }} | |
+ textAlign={undefined} | |
/> | |
), | |
}), | |
@@ -227,7 +266,7 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
size="sm" | |
colorScheme="red" | |
onFocus={(e) => e.currentTarget.blur()} | |
- onClick={(e) => { | |
+ onClick={() => { | |
item.removeItem(item.item.id); | |
}} | |
aria-label={""} | |
@@ -251,6 +290,7 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
setItemDetails, | |
removeItem, | |
taxExempt, | |
+ calculateCartPrice, | |
}))} | |
/> | |
{selectedItemDetails && ( | |
@@ -264,12 +304,9 @@ export const SalesScreenItemsTable: React.FC<SalesScreenItemsTableProps> = ({ | |
const itemDetails = m.get(item.id); | |
if (itemDetails) { | |
itemDetails.item = item; | |
- itemDetails.totalPrice = calculateTotalPrice( | |
- itemDetails, | |
- taxExempt, | |
- ); | |
m.set(item.id, itemDetails); | |
} | |
+ calculateCartPrice(m); | |
return m; | |
}); | |
}} | |
diff --git a/src/components/Table/CurrencyRow.tsx b/src/components/Table/CurrencyRow.tsx | |
index fb5dbd5..a361de2 100644 | |
--- a/src/components/Table/CurrencyRow.tsx | |
+++ b/src/components/Table/CurrencyRow.tsx | |
@@ -1,20 +1,18 @@ | |
-import { Box, BoxProps } from "@chakra-ui/react"; | |
+import { Box, BoxProps, forwardRef } from "@chakra-ui/react"; | |
import { Currency, currencyFormat } from "../../utils/currencyUtils"; | |
-export interface CurrencyRowProps { | |
+export interface CurrencyRowProps extends BoxProps { | |
currency?: Currency; | |
value?: number; | |
- containerProps?: BoxProps; | |
} | |
-export const CurrencyRow: React.FC<CurrencyRowProps> = ({ | |
- currency = "USD", | |
- value, | |
- containerProps, | |
-}) => { | |
+export const CurrencyRow: React.FC<CurrencyRowProps> = forwardRef< | |
+ CurrencyRowProps, | |
+ "div" | |
+>(({ currency = "USD", value, ...containerProps }, ref) => { | |
return ( | |
- <Box w="100%" textAlign="right" {...containerProps}> | |
+ <Box w="100%" textAlign="right" ref={ref} {...containerProps}> | |
{currencyFormat(currency, value)} | |
</Box> | |
); | |
-}; | |
+}); | |
diff --git a/src/components/Table/TransformityTable.tsx b/src/components/Table/TransformityTable.tsx | |
index 6aab01f..b9e13f4 100644 | |
--- a/src/components/Table/TransformityTable.tsx | |
+++ b/src/components/Table/TransformityTable.tsx | |
@@ -103,6 +103,10 @@ export function TransformityTable<T>({ | |
getSortedRowModel: getSortedRowModel(), | |
getExpandedRowModel: getExpandedRowModel(), | |
getSubRows, | |
+ state: { | |
+ sorting: sortingState, | |
+ expanded: expanded, | |
+ }, | |
onExpandedChange: (updaterOrValue) => { | |
if (typeof updaterOrValue === "function") { | |
expanded && setExpanded && setExpanded(updaterOrValue(expanded)); | |
diff --git a/src/components/TransactionByDate.tsx b/src/components/TransactionByDate.tsx | |
index c6bbe62..8c09c0d 100644 | |
--- a/src/components/TransactionByDate.tsx | |
+++ b/src/components/TransactionByDate.tsx | |
@@ -17,6 +17,7 @@ import { FaPrint } from "react-icons/fa"; | |
import { Link } from "react-router-dom"; | |
import { TransactionReportByDateOutput } from "../model/data-contracts"; | |
import { USDollar } from "../pages/SalePage/SalePage"; | |
+import { calculateSubtotal } from "../utils/numberUtils"; | |
export type TransactionByDateProps = { | |
data: TransactionReportByDateOutput; | |
@@ -33,12 +34,12 @@ function itemComp(transactions: TransactionReportByDateOutput["transactions"]) { | |
let environmentFee = 0; | |
let discount = 0; | |
- for (const item of transaction.transactionItems) { | |
- tax += item.tax; | |
- subTotal += item.price * item.quantity; | |
- bottleFee += item.bottleFee; | |
- environmentFee += item.environmentFee; | |
- discount += item.discount; | |
+ for (const transactionItem of transaction.transactionItems) { | |
+ tax += transactionItem.tax; | |
+ subTotal += calculateSubtotal(transactionItem); | |
+ bottleFee += transactionItem.bottleFee; | |
+ environmentFee += transactionItem.environmentFee; | |
+ discount += transactionItem.discount; | |
} | |
items.push( | |
@@ -94,7 +95,7 @@ const TransactionByDate = ({ data }: TransactionByDateProps) => { | |
0, | |
); | |
totalSubtotal += transactionItems.reduce( | |
- (partialSum, a) => partialSum + a.price * a.quantity, | |
+ (partialSum, a) => partialSum + calculateSubtotal(a), | |
0, | |
); | |
totalBottleFee += transactionItems.reduce( | |
diff --git a/src/components/TransactionsComponents/SummaryTable.tsx b/src/components/TransactionsComponents/SummaryTable.tsx | |
index 9afed95..7fb6f48 100644 | |
--- a/src/components/TransactionsComponents/SummaryTable.tsx | |
+++ b/src/components/TransactionsComponents/SummaryTable.tsx | |
@@ -87,7 +87,14 @@ export const SummaryTable = ({ | |
{body.map((b, i) => ( | |
<Tr key={i}> | |
{b.map((j, k) => ( | |
- <Td key={k}>{j}</Td> | |
+ <Td | |
+ data-testid={`${header.at(k)}-${i}-${k}` | |
+ .replaceAll(" ", "-") | |
+ .toLowerCase()} | |
+ key={k} | |
+ > | |
+ {j} | |
+ </Td> | |
))} | |
</Tr> | |
))} | |
diff --git a/src/components/TransactionsComponents/TransactionsReportTable.tsx b/src/components/TransactionsComponents/TransactionsReportTable.tsx | |
index 764b0ff..21001a6 100644 | |
--- a/src/components/TransactionsComponents/TransactionsReportTable.tsx | |
+++ b/src/components/TransactionsComponents/TransactionsReportTable.tsx | |
@@ -45,6 +45,7 @@ export const TransactionsReportTable: React.FC< | |
cell: ({ getValue }) => <CurrencyRow value={getValue()} />, | |
footer: ({ table }) => ( | |
<CurrencyRow | |
+ data-testid="tax-footer" | |
value={table | |
.getRowModel() | |
.rows.reduce((acc, row) => acc + row.original.tax, 0)} | |
@@ -57,6 +58,7 @@ export const TransactionsReportTable: React.FC< | |
cell: ({ getValue }) => <CurrencyRow value={getValue()} />, | |
footer: ({ table }) => ( | |
<CurrencyRow | |
+ data-testid="subtotal-footer" | |
value={table | |
.getRowModel() | |
.rows.reduce((acc, row) => acc + row.original.subtotal, 0)} | |
@@ -69,6 +71,7 @@ export const TransactionsReportTable: React.FC< | |
cell: ({ getValue }) => <CurrencyRow value={getValue()} />, | |
footer: ({ table }) => ( | |
<CurrencyRow | |
+ data-testid="bottle-deposit-footer" | |
value={table | |
.getRowModel() | |
.rows.reduce((acc, row) => acc + row.original.bottleDeposit, 0)} | |
@@ -81,6 +84,7 @@ export const TransactionsReportTable: React.FC< | |
cell: ({ getValue }) => <CurrencyRow value={getValue()} />, | |
footer: ({ table }) => ( | |
<CurrencyRow | |
+ data-testid="environment-fee-footer" | |
value={table | |
.getRowModel() | |
.rows.reduce((acc, row) => acc + row.original.environmentFee, 0)} | |
@@ -93,6 +97,7 @@ export const TransactionsReportTable: React.FC< | |
cell: ({ getValue }) => <CurrencyRow value={getValue()} />, | |
footer: ({ table }) => ( | |
<CurrencyRow | |
+ data-testid="discount-footer" | |
value={table | |
.getRowModel() | |
.rows.reduce((acc, row) => acc + row.original.discount, 0)} | |
@@ -111,6 +116,7 @@ export const TransactionsReportTable: React.FC< | |
(acc, row) => acc + row.original.transactionTotal, | |
0, | |
)} | |
+ data-testid="transaction-total-footer" | |
/> | |
), | |
}), | |
@@ -142,7 +148,7 @@ export const TransactionsReportTable: React.FC< | |
}, | |
}), | |
columnHelper.accessor((tx) => tx.allNames.substring(0, 200), { | |
- header: "Items(s) Sold", | |
+ header: "Item(s) Sold", | |
}), | |
columnHelper.display({ | |
id: "actions", | |
diff --git a/src/components/UnauthenticatedUserOnlyRoute.tsx b/src/components/UnauthenticatedUserOnlyRoute.tsx | |
index 0bc6a75..e9650b6 100644 | |
--- a/src/components/UnauthenticatedUserOnlyRoute.tsx | |
+++ b/src/components/UnauthenticatedUserOnlyRoute.tsx | |
@@ -11,7 +11,7 @@ const UnauthenticatedUserOnlyRoute: React.FC<{}> = () => { | |
} else if (user === null) { | |
return <Outlet />; | |
} else { | |
- return <Navigate to="/sale" />; | |
+ return <Navigate to="/sale/" />; | |
} | |
}; | |
diff --git a/src/components/VoidSale.tsx b/src/components/VoidSale.tsx | |
index 9f0cfdc..1e5e95a 100644 | |
--- a/src/components/VoidSale.tsx | |
+++ b/src/components/VoidSale.tsx | |
@@ -6,12 +6,22 @@ import { | |
AlertDialogHeader, | |
AlertDialogOverlay, | |
Button, | |
+ HStack, | |
Input, | |
+ Modal, | |
+ ModalBody, | |
+ ModalCloseButton, | |
+ ModalContent, | |
+ ModalHeader, | |
+ ModalOverlay, | |
+ PinInput, | |
+ PinInputField, | |
Spacer, | |
Text, | |
useDisclosure, | |
useToast, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useEffect, useRef, useState } from "react"; | |
import { useNavigate } from "react-router-dom"; | |
import { useTransactionApi } from "../config/ItemsApi"; | |
@@ -51,10 +61,13 @@ const VoidSale = ({ txId, isVoided, inputtedItemDetails }: VoidSaleType) => { | |
setTransactionOutput(inputtedItemDetails); | |
}, [inputtedItemDetails]); | |
+ // TEMPORARY FOR BROWNSTONE | |
+ const approvalModal = useDisclosure(); | |
+ | |
useEffect(() => { | |
if (isVoided === true) return; | |
if (inputtedItemDetails) return; | |
- if (!blockingModal.isOpen) return; | |
+ if (!(blockingModal.isOpen || approvalModal.isOpen)) return; | |
transactionApi?.getTransactionById(txId).then((res) => { | |
setTransactionOutput(res.data); | |
@@ -65,6 +78,7 @@ const VoidSale = ({ txId, isVoided, inputtedItemDetails }: VoidSaleType) => { | |
transactionApi, | |
txId, | |
inputtedItemDetails, | |
+ approvalModal.isOpen, | |
]); | |
const VOID_SALE = ["Void"].map((value) => { | |
@@ -74,9 +88,18 @@ const VoidSale = ({ txId, isVoided, inputtedItemDetails }: VoidSaleType) => { | |
allowOverride | |
key={value} | |
colorScheme="red" | |
- isDisabled={transactionOutput?.isVoided || isVoided} | |
+ isDisabled={ | |
+ transactionOutput?.isVoided || | |
+ isVoided || | |
+ !(transactionOutput?.isTransactionOpen ?? true) | |
+ } | |
onClick={() => { | |
if (!txId) return; | |
+ // TEMPORARY FOR BROWNSTONE | |
+ if (entity?.id === 6) { | |
+ approvalModal.onOpen(); | |
+ return; | |
+ } | |
blockingModal.onOpen(); | |
}} | |
> | |
@@ -136,7 +159,9 @@ const VoidSale = ({ txId, isVoided, inputtedItemDetails }: VoidSaleType) => { | |
</Button> | |
<Spacer /> | |
<Button | |
- isDisabled={disableButton} | |
+ isDisabled={ | |
+ disableButton || !transactionOutput.isTransactionOpen | |
+ } | |
isLoading={isVoiding} | |
onClick={async () => { | |
if (!transactionApi) return; | |
@@ -158,6 +183,7 @@ const VoidSale = ({ txId, isVoided, inputtedItemDetails }: VoidSaleType) => { | |
duration: 5000, | |
isClosable: true, | |
}); | |
+ Sentry.captureException(err); | |
} finally { | |
blockingModal.onClose(); | |
} | |
@@ -169,8 +195,64 @@ const VoidSale = ({ txId, isVoided, inputtedItemDetails }: VoidSaleType) => { | |
</AlertDialogContent> | |
</AlertDialogOverlay> | |
</AlertDialog> | |
+ <ManagerPinInputModal | |
+ {...approvalModal} | |
+ onSuccess={() => { | |
+ blockingModal.onOpen(); | |
+ }} | |
+ /> | |
</> | |
); | |
}; | |
+// TEMPORARY FOR BROWNSTONE | |
+type ManagerPinInputModalProps = { | |
+ isOpen: boolean; | |
+ onClose: () => void; | |
+ onSuccess: () => void; | |
+}; | |
+ | |
+const ManagerPinInputModal: React.FC<ManagerPinInputModalProps> = ({ | |
+ isOpen, | |
+ onClose, | |
+ onSuccess, | |
+}) => { | |
+ const [approvalCode, setApprovalCode] = useState<string>(""); | |
+ | |
+ useEffect(() => { | |
+ if (approvalCode === "2285") { | |
+ setApprovalCode(""); | |
+ onSuccess(); | |
+ onClose(); | |
+ } | |
+ }, [approvalCode, onSuccess, onClose]); | |
+ | |
+ return ( | |
+ <Modal isOpen={isOpen} onClose={onClose}> | |
+ <ModalOverlay /> | |
+ <ModalContent> | |
+ <ModalHeader>Requires Manager Approval</ModalHeader> | |
+ <ModalCloseButton /> | |
+ <ModalBody> | |
+ <HStack w="100%" justifyContent="center"> | |
+ <PinInput | |
+ otp | |
+ mask | |
+ size="lg" | |
+ onChange={setApprovalCode} | |
+ value={approvalCode} | |
+ autoFocus={true} | |
+ > | |
+ <PinInputField autoComplete="off" /> | |
+ <PinInputField autoComplete="off" /> | |
+ <PinInputField autoComplete="off" /> | |
+ <PinInputField autoComplete="off" /> | |
+ </PinInput> | |
+ </HStack> | |
+ </ModalBody> | |
+ </ModalContent> | |
+ </Modal> | |
+ ); | |
+}; | |
+ | |
export default VoidSale; | |
diff --git a/src/components/common/BackButton.tsx b/src/components/common/BackButton.tsx | |
new file mode 100644 | |
index 0000000..de0ad1d | |
--- /dev/null | |
+++ b/src/components/common/BackButton.tsx | |
@@ -0,0 +1,18 @@ | |
+import { Button, ButtonProps } from "@chakra-ui/react"; | |
+import { useNavigate } from "react-router-dom"; | |
+ | |
+export type BackButtonProps = ButtonProps & { | |
+ children?: React.ReactNode; | |
+}; | |
+ | |
+export const BackButton: React.FC<BackButtonProps> = ({ | |
+ children, | |
+ ...props | |
+}) => { | |
+ const navigate = useNavigate(); | |
+ return ( | |
+ <Button variant="ghost" onClick={() => navigate(-1)} {...props}> | |
+ {children ?? "Back"} | |
+ </Button> | |
+ ); | |
+}; | |
diff --git a/src/components/common/CardList/CardList.tsx b/src/components/common/CardList/CardList.tsx | |
index 97f1707..fdd190d 100644 | |
--- a/src/components/common/CardList/CardList.tsx | |
+++ b/src/components/common/CardList/CardList.tsx | |
@@ -1,4 +1,4 @@ | |
-import { Center, Skeleton, StackProps, VStack } from "@chakra-ui/react"; | |
+import { Box, Center, Skeleton, StackProps, VStack } from "@chakra-ui/react"; | |
export type CardListProps<T> = { | |
items: T[]; | |
@@ -30,7 +30,7 @@ export function CardList<T>({ | |
justifyContent="space-between" | |
{...containerProps} | |
> | |
- <VStack w="100%" h="100%"> | |
+ <VStack data-testid="card-list" w="100%" h="100%" flexGrow={2}> | |
{isLoading | |
? Array.from({ length: 4 }).map((_, i) => ( | |
<Skeleton key={i} w="100%" h={`${estimatedItemHeight ?? 50}px`} /> | |
@@ -42,7 +42,7 @@ export function CardList<T>({ | |
</Center> | |
)} | |
</VStack> | |
- {children} | |
+ <Box w="100%">{children}</Box> | |
</VStack> | |
); | |
} | |
diff --git a/src/components/common/EditableInput/EditableInput2.tsx b/src/components/common/EditableInput/EditableInput2.tsx | |
index b00a9b7..cf82825 100644 | |
--- a/src/components/common/EditableInput/EditableInput2.tsx | |
+++ b/src/components/common/EditableInput/EditableInput2.tsx | |
@@ -1,4 +1,5 @@ | |
import { Input, InputProps, Text, TextProps } from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useEffect, useRef, useState } from "react"; | |
import { | |
InputConverter, | |
@@ -127,7 +128,9 @@ export function EditableInput<T extends string | number>({ | |
const val = converter.parse(e.target.value); | |
setDisplayValue(converter.format(val)); | |
onChange?.(e.target.value, isMoney || isNumber ? val : undefined); | |
- } catch (e) {} | |
+ } catch (e) { | |
+ Sentry.captureException(e); | |
+ } | |
}} | |
{...inputProps} | |
/> | |
diff --git a/src/components/common/NumberInputWithSideSteppers.tsx b/src/components/common/NumberInputWithSideSteppers.tsx | |
index 3505f63..55ae98a 100644 | |
--- a/src/components/common/NumberInputWithSideSteppers.tsx | |
+++ b/src/components/common/NumberInputWithSideSteppers.tsx | |
@@ -33,23 +33,26 @@ export const NumberInputWithSideSteppers = ({ | |
icon={<MinusIcon />} | |
variant={"ghost"} | |
onFocus={(e) => e.currentTarget.blur()} | |
- isDisabled={!isEditing || (min !== undefined && props.value <= min)} | |
onClick={() => { | |
- const newValue = props.value - step; | |
- if (min === undefined || newValue >= min) { | |
- props.onChange(newValue); | |
- } | |
+ let value = props.value - (step ?? 1); | |
+ if (value < -1000) value = -1000; | |
+ if (value > 1000) value = 1000; | |
+ return props.onChange(value); | |
}} | |
/> | |
<NumberInput | |
- isDisabled={!isEditing} | |
+ name="quantity" | |
value={props.value} | |
defaultValue={props.value} | |
precision={props.precision} | |
- min={min} | |
+ max={1000} | |
+ min={-1000} | |
width={"auto"} | |
onChange={(valueString) => { | |
- props.onChange(parseInt(valueString)); | |
+ let value = parseInt(valueString); | |
+ if (value > 1000) value = 1000; | |
+ if (value < -1000) value = -1000; | |
+ props.onChange(value); | |
}} | |
> | |
<NumberInputField onFocus={(item) => item.currentTarget.select()} /> | |
@@ -63,10 +66,11 @@ export const NumberInputWithSideSteppers = ({ | |
onFocus={(e) => e.currentTarget.blur()} | |
isDisabled={!isEditing || (max !== undefined && props.value >= max)} | |
onClick={() => { | |
- const newValue = props.value + step; | |
- if (max === undefined || newValue <= max) { | |
- props.onChange(newValue); | |
- } | |
+ let value = props.value + (step ?? 1); | |
+ | |
+ if (value < -1000) value = -1000; | |
+ if (value > 1000) value = 1000; | |
+ return props.onChange(value); | |
}} | |
/> | |
</HStack> | |
diff --git a/src/components/common/RadioCard/RadioCardV1.tsx b/src/components/common/RadioCard/RadioCardV1.tsx | |
new file mode 100644 | |
index 0000000..7f7b38e | |
--- /dev/null | |
+++ b/src/components/common/RadioCard/RadioCardV1.tsx | |
@@ -0,0 +1,40 @@ | |
+import { Box, useRadio, UseRadioProps } from "@chakra-ui/react"; | |
+ | |
+export interface RadioCardV1Props extends UseRadioProps { | |
+ children: React.ReactNode; | |
+} | |
+ | |
+export const RadioCardV1: React.FC<RadioCardV1Props> = ({ | |
+ children, | |
+ ...props | |
+}) => { | |
+ const { getInputProps, getRadioProps } = useRadio(props); | |
+ | |
+ const input = getInputProps(); | |
+ const checkbox = getRadioProps(); | |
+ | |
+ return ( | |
+ <Box as="label"> | |
+ <input {...input} /> | |
+ <Box | |
+ {...checkbox} | |
+ cursor="pointer" | |
+ borderWidth="1px" | |
+ borderRadius="md" | |
+ boxShadow="md" | |
+ _checked={{ | |
+ bg: "blue.600", | |
+ color: "white", | |
+ borderColor: "blue.600", | |
+ }} | |
+ _focus={{ | |
+ boxShadow: "outline", | |
+ }} | |
+ px={5} | |
+ py={1} | |
+ > | |
+ {children} | |
+ </Box> | |
+ </Box> | |
+ ); | |
+}; | |
diff --git a/src/components/common/StyledPaginationControls/PaginationComponents.tsx b/src/components/common/StyledPaginationControls/PaginationComponents.tsx | |
index bc8a6f0..f5ed582 100644 | |
--- a/src/components/common/StyledPaginationControls/PaginationComponents.tsx | |
+++ b/src/components/common/StyledPaginationControls/PaginationComponents.tsx | |
@@ -57,6 +57,7 @@ const PaginationPrevious: React.FC<LinkProps & { isDisabled?: boolean }> = ({ | |
> | |
<IconButton | |
variant={"outline"} | |
+ data-testid="pagination-previous" | |
aria-label="Go to previous page" | |
icon={<ChevronLeftIcon />} | |
isDisabled={isDisabled} | |
@@ -73,6 +74,7 @@ const PaginationNext: React.FC<LinkProps & { isDisabled?: boolean }> = ({ | |
<Link {...props}> | |
<IconButton | |
variant={"outline"} | |
+ data-testid="pagination-next" | |
aria-label="Go to next page" | |
icon={<ChevronRightIcon />} | |
isDisabled={isDisabled} | |
diff --git a/src/components/navbar/PopRegisterButton.tsx b/src/components/navbar/PopRegisterButton.tsx | |
index e1182da..736fafd 100644 | |
--- a/src/components/navbar/PopRegisterButton.tsx | |
+++ b/src/components/navbar/PopRegisterButton.tsx | |
@@ -8,8 +8,9 @@ export const PopRegisterButton = () => { | |
const [device, cashDrawerOnOpen] = useCashDrawer(); | |
const [entity] = useEntitySelected(); | |
+ // TODO: remove Knotty Pine check after RBAC setup | |
// If there isn't a device, return null | |
- if (!device) return null; | |
+ if (!device || entity.id === 5) return null; | |
return ( | |
<Tooltip hasArrow label={"Pop cash drawer"} placement={"bottom"}> | |
diff --git a/src/components/navbar/StoreInfo.tsx b/src/components/navbar/StoreInfo.tsx | |
index 7620370..e165dd4 100644 | |
--- a/src/components/navbar/StoreInfo.tsx | |
+++ b/src/components/navbar/StoreInfo.tsx | |
@@ -9,7 +9,7 @@ export const StoreInfo: React.FC<StoreInfoProps> = ({}) => { | |
const [entity] = useEntitySelected(); | |
return ( | |
- <VStack spacing={0} alignItems="end"> | |
+ <VStack data-testid="store-info" spacing={0} alignItems="end"> | |
<Text> | |
<strong>{entity?.name}</strong> | |
</Text> | |
diff --git a/src/components/navbar/StoreInfoWithSelect.tsx b/src/components/navbar/StoreInfoWithSelect.tsx | |
index 99193bc..d54d7e9 100644 | |
--- a/src/components/navbar/StoreInfoWithSelect.tsx | |
+++ b/src/components/navbar/StoreInfoWithSelect.tsx | |
@@ -1,5 +1,12 @@ | |
import { ChevronDownIcon } from "@chakra-ui/icons"; | |
-import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react"; | |
+import { | |
+ Button, | |
+ Menu, | |
+ MenuButton, | |
+ MenuItem, | |
+ MenuList, | |
+ Portal, | |
+} from "@chakra-ui/react"; | |
import { useAuthorization } from "../../context/AuthorizationContext/AuthorizationContext"; | |
import { useEntitySelected } from "../../context/EntityProvider"; | |
import { useRegisterProvider } from "../../context/RegisterProvider"; | |
@@ -18,27 +25,35 @@ export const StoreInfoWithSelect: React.FC<StoreInfoWithSelectProps> = ({}) => { | |
} | |
return ( | |
- <Menu isLazy> | |
- <MenuButton as={Button} variant="outline" rightIcon={<ChevronDownIcon />}> | |
+ <Menu> | |
+ <MenuButton | |
+ data-testid="store-info-select" | |
+ as={Button} | |
+ variant="outline" | |
+ rightIcon={<ChevronDownIcon />} | |
+ onFocus={(e) => e.currentTarget.blur()} | |
+ > | |
<StoreInfo /> | |
</MenuButton> | |
- <MenuList> | |
- {authorizedUser?.user?.entities?.map((entity: UserEntityAuth) => ( | |
- <MenuItem | |
- key={entity.entity.id} | |
- onClick={() => { | |
- setSelectedEntity(entity.entity); | |
- setRegister({ | |
- registerNumber: entity.entity.registers[0].registerId, | |
- isRegisterOpen: true, | |
- openDateTime: new Date().toISOString(), | |
- }); | |
- }} | |
- > | |
- {entity.entity.name} | |
- </MenuItem> | |
- ))} | |
- </MenuList> | |
+ <Portal> | |
+ <MenuList> | |
+ {authorizedUser?.user?.entities?.map((entity: UserEntityAuth) => ( | |
+ <MenuItem | |
+ key={entity.entity.id} | |
+ onClick={() => { | |
+ setSelectedEntity(entity.entity); | |
+ setRegister({ | |
+ registerNumber: entity.entity.registers[0].registerId, | |
+ isRegisterOpen: true, | |
+ openDateTime: new Date().toISOString(), | |
+ }); | |
+ }} | |
+ > | |
+ {entity.entity.name} | |
+ </MenuItem> | |
+ ))} | |
+ </MenuList> | |
+ </Portal> | |
</Menu> | |
); | |
}; | |
diff --git a/src/components/promotions/PromotionMatcher/PromotionMatcherForm.tsx b/src/components/promotions/PromotionMatcher/PromotionMatcherForm.tsx | |
index 70afdcb..d169c26 100644 | |
--- a/src/components/promotions/PromotionMatcher/PromotionMatcherForm.tsx | |
+++ b/src/components/promotions/PromotionMatcher/PromotionMatcherForm.tsx | |
@@ -75,7 +75,7 @@ export const PromotionMatcherForm: React.FC<PromotionMatcherFormProps> = ({ | |
}, [defaultMatchers, matchers, uncontrolledMatchers.length]); | |
return ( | |
- <VStack alignItems={"flex-start"} w="100%"> | |
+ <VStack alignItems={"flex-start"} w="100%" h="100%"> | |
<CardList | |
emptyText="No matchers" | |
items={displayedMatchers} | |
diff --git a/src/components/promotions/PromotionsForm.tsx b/src/components/promotions/PromotionsForm.tsx | |
index 48453a0..75e2766 100644 | |
--- a/src/components/promotions/PromotionsForm.tsx | |
+++ b/src/components/promotions/PromotionsForm.tsx | |
@@ -139,11 +139,7 @@ export const PromotionsForm: React.FC<PromotionsFormProps> = ({ | |
}} | |
/> | |
</HStack> | |
- <HStack | |
- w="100%" | |
- justifyContent="space-between" | |
- alignItems="flex-start" | |
- > | |
+ <HStack w="100%" justifyContent="space-between" alignItems="stretch"> | |
<VStack alignItems="flex-start" w="50%"> | |
<FormLabel>Include Items</FormLabel> | |
<PromotionMatcherForm | |
diff --git a/src/components/promotions/PromotionsList.tsx b/src/components/promotions/PromotionsList.tsx | |
index f74af5d..d1788a6 100644 | |
--- a/src/components/promotions/PromotionsList.tsx | |
+++ b/src/components/promotions/PromotionsList.tsx | |
@@ -65,6 +65,10 @@ export const PromotionsList: React.FC<PromotionsListProps> = ({ | |
return ( | |
<VStack w="100%" h="100%" justifyContent="space-between" flexGrow={2}> | |
<CardList | |
+ containerProps={{ | |
+ // @ts-ignore | |
+ "data-testid": "promotions-list", | |
+ }} | |
isLoading={isLoading} | |
items={promotions} | |
estimatedItemHeight={100} | |
diff --git a/src/config/ItemsApi.ts b/src/config/ItemsApi.ts | |
index ce0b1f4..e06baf9 100644 | |
--- a/src/config/ItemsApi.ts | |
+++ b/src/config/ItemsApi.ts | |
@@ -1,7 +1,7 @@ | |
-import * as Sentry from "@sentry/react"; | |
import { useEffect, useState } from "react"; | |
import { useUserAuth } from "../context/AuthenticationContext/AuthenticationContext"; | |
import { Canny } from "../model/Canny"; | |
+import { Cart } from "../model/Cart"; | |
import { Department } from "../model/Department"; | |
import { Entity } from "../model/Entity"; | |
import { GiftCardApi } from "../model/GiftCardApi"; | |
@@ -56,10 +56,6 @@ export function useBackendAxios() { | |
} | |
return config; | |
}); | |
- i.instance.interceptors.response.use(undefined, (error) => { | |
- Sentry.captureException(error); | |
- return Promise.reject(error); | |
- }); | |
setAxiosInstance(i); | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [firebaseAuth.currentUser]); | |
@@ -199,9 +195,21 @@ export function useInvoiceApi() { | |
return api; | |
} | |
+export function useCartApi() { | |
+ const [api, setApi] = useState<Cart>(); | |
+ const axiosInstance = axios; | |
+ | |
+ useEffect(() => { | |
+ if (!axiosInstance) return; | |
+ setApi(new Cart(axiosInstance)); | |
+ }, [axiosInstance]); | |
+ | |
+ return api; | |
+} | |
+ | |
export function usePromotionApi() { | |
const [api, setApi] = useState<PromotionApi>(); | |
- const axiosInstance = useBackendAxios(); | |
+ const axiosInstance = axios; | |
useEffect(() => { | |
if (!axiosInstance) return; | |
diff --git a/src/context/AuthorizationContext/AuthorizationContext.tsx b/src/context/AuthorizationContext/AuthorizationContext.tsx | |
index 6f46796..9dc3f72 100644 | |
--- a/src/context/AuthorizationContext/AuthorizationContext.tsx | |
+++ b/src/context/AuthorizationContext/AuthorizationContext.tsx | |
@@ -1,3 +1,4 @@ | |
+import { Button } from "@chakra-ui/react"; | |
import { jwtDecode } from "jwt-decode"; | |
import { | |
createContext, | |
@@ -13,6 +14,7 @@ import { | |
firebaseAuthStore, | |
} from "../../config/Firebase/firebase"; | |
import { AuthorizedUserDTO, EntityDTO } from "../../model/data-contracts"; | |
+import { UnauthorizedPage } from "../../pages/Auth/UnauthorizedPage"; | |
import { ErrorPage } from "../../pages/ErrorPage"; | |
import { Permission } from "../../utils/Permission"; | |
import { useUserAuth } from "../AuthenticationContext/AuthenticationContext"; | |
@@ -200,7 +202,21 @@ export const AuthorizationContextProvider = ({ | |
}, [isAuthenticationLoading, refreshPermissions]); | |
if (error) { | |
- return <ErrorPage error={error} />; | |
+ if (error.errorCode === 401) { | |
+ return ( | |
+ <UnauthorizedPage showNavbar={false} showBackButton={false}> | |
+ <Button | |
+ onClick={async () => { | |
+ await handleLogout(); | |
+ return window.location.reload(); | |
+ }} | |
+ > | |
+ Logout | |
+ </Button> | |
+ </UnauthorizedPage> | |
+ ); | |
+ } | |
+ return <ErrorPage error={error.message} />; | |
} | |
return ( | |
diff --git a/src/context/AuthorizationContext/useAuthorizeEmployee.tsx b/src/context/AuthorizationContext/useAuthorizeEmployee.tsx | |
index 789c88d..64a3308 100644 | |
--- a/src/context/AuthorizationContext/useAuthorizeEmployee.tsx | |
+++ b/src/context/AuthorizationContext/useAuthorizeEmployee.tsx | |
@@ -1,3 +1,4 @@ | |
+import * as Sentry from "@sentry/react"; | |
import { AxiosError } from "axios"; | |
import { useCallback, useState } from "react"; | |
import { useUserApi } from "../../config/ItemsApi"; | |
@@ -35,8 +36,10 @@ export const useAuthorizeEmployee = () => { | |
const error = e as AxiosError; | |
if (error.response?.status === 401) { | |
setError("Invalid PIN"); | |
+ return; | |
} | |
} | |
+ Sentry.captureException(e); | |
} | |
}; | |
return [authorizeEmployee, { isLoading, error, clear }] as const; | |
diff --git a/src/context/AuthorizationContext/useAuthorizeUser.tsx b/src/context/AuthorizationContext/useAuthorizeUser.tsx | |
index e4ba28b..8f608a7 100644 | |
--- a/src/context/AuthorizationContext/useAuthorizeUser.tsx | |
+++ b/src/context/AuthorizationContext/useAuthorizeUser.tsx | |
@@ -1,3 +1,4 @@ | |
+import * as Sentry from "@sentry/react"; | |
import { AxiosError } from "axios"; | |
import { useCallback, useState } from "react"; | |
import { useUserApi } from "../../config/ItemsApi"; | |
@@ -5,7 +6,7 @@ import { useUserApi } from "../../config/ItemsApi"; | |
export const useAuthorizeUser = () => { | |
const userApi = useUserApi(); | |
const [isLoading, setIsLoading] = useState(false); | |
- const [error, setError] = useState<string>(); | |
+ const [error, setError] = useState<{ message: string; errorCode?: number }>(); | |
const clear = useCallback(() => { | |
setError(undefined); | |
@@ -20,22 +21,22 @@ export const useAuthorizeUser = () => { | |
setIsLoading(false); | |
if (response?.data.token) { | |
return response.data; | |
- // setStoreUser(user); | |
- // setSelectedEmployee(undefined); | |
- // await customTokenSignIn(response.data.token); | |
} | |
} catch (e: any) { | |
setIsLoading(false); | |
if (e.isAxiosError) { | |
const error = e as AxiosError; | |
if (error.response?.status === 401) { | |
- setError("Invalid PIN"); | |
+ setError({ message: "Unauthorized", errorCode: 401 }); | |
+ return; | |
} else { | |
- setError("Something went wrong"); | |
+ setError({ message: "Something went wrong" }); | |
+ return; | |
} | |
} else { | |
- setError("Something went wrong"); | |
+ setError({ message: "Something went wrong" }); | |
} | |
+ Sentry.captureException(e); | |
} | |
}, [userApi]); | |
return [authorizeUser, { isLoading, error, clear }] as const; | |
diff --git a/src/context/EntityProvider.tsx b/src/context/EntityProvider.tsx | |
index 68109ad..ce4afa6 100644 | |
--- a/src/context/EntityProvider.tsx | |
+++ b/src/context/EntityProvider.tsx | |
@@ -15,6 +15,7 @@ import { useUserAuth } from "./AuthenticationContext/AuthenticationContext"; | |
export type EntityContextType = { | |
entity?: EntityDTO; | |
refreshEntity: (e?: EntityDTO) => void; | |
+ isLoading: boolean; | |
}; | |
export const EntityContext = createContext<EntityContextType | undefined>( | |
@@ -57,6 +58,7 @@ export const EntityContextProvider = ({ | |
} | |
const getEntity = async () => { | |
+ setIsLoading(true); | |
const e = await entityApi?.getEntityById(entityId); | |
return e.data; | |
}; | |
@@ -78,23 +80,34 @@ export const EntityContextProvider = ({ | |
} | |
return ( | |
- <EntityContext.Provider value={{ entity, refreshEntity }}> | |
+ <EntityContext.Provider value={{ entity, refreshEntity, isLoading }}> | |
{children} | |
</EntityContext.Provider> | |
); | |
}; | |
-/** Use anywhere where entity is not yet set/required */ | |
-export function useEntity(): [EntityDTO | undefined, (e?: EntityDTO) => void] { | |
+export function useEntity(): [ | |
+ EntityDTO | undefined, | |
+ (e?: EntityDTO) => void, | |
+ boolean, | |
+] { | |
const entityContext = useContext(EntityContext); | |
if (entityContext === undefined) { | |
- throw new Error("useEntity must be used within a EntityProvider"); | |
+ throw new Error("useEntitySelected must be used within a EntityProvider"); | |
} | |
- return [entityContext.entity, entityContext.refreshEntity]; | |
+ return [ | |
+ entityContext.entity, | |
+ entityContext.refreshEntity, | |
+ entityContext.isLoading, | |
+ ]; | |
} | |
-export function useEntitySelected(): [EntityDTO, (e?: EntityDTO) => void] { | |
+export function useEntitySelected(): [ | |
+ EntityDTO, | |
+ (e?: EntityDTO) => void, | |
+ false, | |
+] { | |
const entityContext = useContext(EntityContext); | |
if (entityContext === undefined) { | |
throw new Error("useEntitySelected must be used within a EntityProvider"); | |
@@ -104,5 +117,5 @@ export function useEntitySelected(): [EntityDTO, (e?: EntityDTO) => void] { | |
throw new Error("Entity must be selected"); | |
} | |
- return [entityContext.entity, entityContext.refreshEntity]; | |
+ return [entityContext.entity, entityContext.refreshEntity, false]; | |
} | |
diff --git a/src/context/InvoiceContext.tsx b/src/context/InvoiceContext.tsx | |
index fce5d12..ad3829d 100644 | |
--- a/src/context/InvoiceContext.tsx | |
+++ b/src/context/InvoiceContext.tsx | |
@@ -1,4 +1,5 @@ | |
import { useToast } from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { ColumnSort, DeepKeys } from "@tanstack/react-table"; | |
import _ from "lodash"; | |
import React, { | |
@@ -127,6 +128,7 @@ export const InvoiceProvider: React.FC<InvoiceProviderProps> = ({ | |
duration: 3000, | |
isClosable: true, | |
}); | |
+ Sentry.captureException(err); | |
} | |
setIsLoading(false); | |
}, | |
diff --git a/src/context/Sales/SalesContext.tsx b/src/context/Sales/SalesContext.tsx | |
index 46f0e11..f0a1d63 100644 | |
--- a/src/context/Sales/SalesContext.tsx | |
+++ b/src/context/Sales/SalesContext.tsx | |
@@ -1,16 +1,18 @@ | |
import { createContext, useContext } from "react"; | |
import { GovernmentID, ItemDetails } from "../../model/data-contracts"; | |
-import { ItemWithQty } from "../../pages/SalePage/SalePageUtils"; | |
+import { LineItem } from "../../pages/SalePage/SalePageUtils"; | |
-interface SalesContextType { | |
+export interface SalesContextType { | |
quantity: number; | |
setQuantity: React.Dispatch<React.SetStateAction<number>>; | |
discount: number; | |
setDiscount: React.Dispatch<React.SetStateAction<number>>; | |
taxExempt: boolean; | |
setTaxExempt: React.Dispatch<React.SetStateAction<boolean>>; | |
- items: Map<number, ItemWithQty>; | |
- setItems: React.Dispatch<React.SetStateAction<Map<number, ItemWithQty>>>; | |
+ items: Map<number, LineItem>; | |
+ setItems: React.Dispatch<React.SetStateAction<Map<number, LineItem>>>; | |
+ addSingleItemToCart: (itemDetailz: ItemDetails, quantity: number) => void; | |
+ calculateCartPrice: (itemDetails: SalesContextType["items"]) => void; | |
governmentID?: GovernmentID; | |
setGovernmentID: React.Dispatch< | |
React.SetStateAction<GovernmentID | undefined> | |
@@ -18,9 +20,9 @@ interface SalesContextType { | |
transactionTotalPrice: () => number; | |
clearCart: () => void; | |
retrieveItemsLikeUpc: (barCode: string, quantity?: number) => void; | |
- multipleChoices: ItemDetails[] | undefined; | |
+ multipleChoices: { items: ItemDetails[]; quantity: number } | undefined; | |
setMultipleChoices: React.Dispatch< | |
- React.SetStateAction<ItemDetails[] | undefined> | |
+ React.SetStateAction<{ items: ItemDetails[]; quantity: number } | undefined> | |
>; | |
} | |
diff --git a/src/context/Sales/SalesContextProvider.tsx b/src/context/Sales/SalesContextProvider.tsx | |
index 155df16..b80cc96 100644 | |
--- a/src/context/Sales/SalesContextProvider.tsx | |
+++ b/src/context/Sales/SalesContextProvider.tsx | |
@@ -1,10 +1,24 @@ | |
+import * as Sentry from "@sentry/react"; | |
import _ from "lodash"; | |
-import { useCallback, useState } from "react"; | |
+import { useCallback, useEffect, useRef, useState } from "react"; | |
+import { v4 as uuidv4 } from "uuid"; | |
+import Loading from "../../components/Loading"; | |
import usePoleDisplay from "../../components/PoleDisplay"; | |
-import { useItemsApi } from "../../config/ItemsApi"; | |
-import { GovernmentID, Item } from "../../model/data-contracts"; | |
-import { addSingleItem, ItemWithQty } from "../../pages/SalePage/SalePageUtils"; | |
+import { | |
+ useCartApi, | |
+ useItemsApi, | |
+ useTransactionApi, | |
+} from "../../config/ItemsApi"; | |
+import { | |
+ GovernmentID, | |
+ Item, | |
+ PricingCartInputItem, | |
+ TransactionCartItem, | |
+ UnknownItem, | |
+} from "../../model/data-contracts"; | |
+import { addSingleItem, LineItem } from "../../pages/SalePage/SalePageUtils"; | |
import { useEntitySelected } from "../EntityProvider"; | |
+import { useRegisterProvider } from "../RegisterProvider"; | |
import { SalesContext } from "./SalesContext"; | |
export interface SalesContextProviderProps { | |
@@ -15,88 +29,290 @@ export const SalesContextProvider: React.FC<SalesContextProviderProps> = ({ | |
}) => { | |
// APIs | |
const api = useItemsApi(); | |
- const [entity] = useEntitySelected(); | |
+ const cartApi = useCartApi(); | |
+ const [entity, , isEntityLoading] = useEntitySelected(); | |
const poleDisplay = usePoleDisplay(); | |
// State | |
- const [items, setItems] = useState<Map<number, ItemWithQty>>(new Map()); | |
- const [multipleChoices, setMultipleChoices] = useState<Item[] | undefined>(); | |
+ const [items, setItems] = useState<Map<number, LineItem>>(new Map()); | |
+ const [multipleChoices, setMultipleChoices] = useState<{ | |
+ items: Item[]; | |
+ quantity: number; | |
+ }>(); | |
const [quantity, setQuantity] = useState(1); | |
const [taxExempt, setTaxExempt] = useState(false); | |
const [discount, setDiscount] = useState(0); | |
const [governmentID, setGovernmentID] = useState<GovernmentID | undefined>(); | |
+ const [requestId] = useState(uuidv4()); | |
+ const transactionApi = useTransactionApi(); | |
+ const { register } = useRegisterProvider(); | |
+ const cartPricingAbortController = useRef<AbortController | null>(null); | |
+ const retrieveUpcAbortController = useRef<AbortController>( | |
+ new AbortController(), | |
+ ); | |
// Callbacks | |
const transactionTotalPrice = useCallback(() => { | |
let price = 0; | |
- for (const [, item] of items) { | |
- price += item.totalPrice; | |
+ for (const [, lineItem] of items) { | |
+ price += lineItem.totalPrice; | |
} | |
return _.round(price, 2); | |
}, [items]); | |
- const retrieveItemsLikeUpc = useCallback( | |
- (barCode?: string, quantity = 1) => { | |
+ // Set items in cart when itemDetailsDebounced changes | |
+ const setItemsInCart = useCallback( | |
+ async (it: Map<number, LineItem>) => { | |
if (!entity?.id) return; | |
- if (barCode && api) { | |
- api | |
- .retrieveItemsLikeUpc({ | |
- upcCode: `%${barCode}%`, | |
+ if (it.size === 0) { | |
+ try { | |
+ await transactionApi?.emptyCart({ | |
entityId: entity.id, | |
- }) | |
- .then((res) => { | |
- if (res.data.length === 1) { | |
- setItems((prev) => { | |
- const m = new Map(prev); | |
- res.data.forEach((retrievedItem) => { | |
- const item = addSingleItem( | |
- retrievedItem, | |
- m, | |
- quantity, | |
- taxExempt, | |
- ); | |
- prev.set(retrievedItem.id, item); | |
- | |
- poleDisplay( | |
- `${item.quantity} ${item.item.name | |
- .trim() | |
- .substring(0, 9)} $${( | |
- item.item.sellPrice - item.discount | |
- ).toFixed(2)}`, | |
- `Grand Total $${transactionTotalPrice().toFixed(2)}`, | |
- ); | |
- m.set(retrievedItem.id, item); | |
- }); | |
- return m; | |
- }); | |
- setQuantity(1); | |
- } else if (res.data.length === 0) { | |
- setMultipleChoices([]); | |
- setQuantity(1); | |
- } else { | |
- setMultipleChoices(res.data); | |
- } | |
- }) | |
- .catch((err) => { | |
- console.error(err.message); | |
- setQuantity(1); | |
+ registerNumber: register.registerNumber, | |
}); | |
+ } catch (err) { | |
+ Sentry.captureException(err); | |
+ console.error(err); | |
+ } | |
+ return; | |
+ } | |
+ | |
+ const cartItems = [...it.values()].map((lineItem) => { | |
+ const cartItem: TransactionCartItem = { | |
+ ...lineItem, | |
+ ...lineItem.item, | |
+ itemId: lineItem.item.id, | |
+ entityId: entity.id, | |
+ price: lineItem.item.sellPrice, | |
+ tax: lineItem.tax, | |
+ bottleFee: lineItem.bottleDeposit, | |
+ environmentFee: lineItem.environmentFee, | |
+ upcCode: lineItem.item.upc, | |
+ department: lineItem.item.department.name, | |
+ registerId: register.registerNumber, | |
+ requestId, | |
+ }; | |
+ return cartItem; | |
+ }); | |
+ try { | |
+ await transactionApi?.setItemsInCart({ | |
+ items: cartItems, | |
+ }); | |
+ } catch (err) { | |
+ Sentry.captureException(err); | |
+ console.error(err); | |
} | |
}, | |
- [api, entity, poleDisplay, taxExempt, transactionTotalPrice], | |
+ [entity?.id, register.registerNumber, requestId, transactionApi], | |
+ ); | |
+ | |
+ useEffect(() => { | |
+ if (items.size !== 0) return; | |
+ setItemsInCart(items); | |
+ }, [items, setItemsInCart]); | |
+ | |
+ const calculateCartPrice = useCallback( | |
+ (itemDetailz: typeof items) => { | |
+ if (!cartApi) return; | |
+ if (!entity?.id) return; | |
+ if (!itemDetailz.size) return; | |
+ | |
+ const pricingCartInputItems: (PricingCartInputItem | UnknownItem)[] = []; | |
+ | |
+ // AbortController setup | |
+ // https://chat.openai.com/share/c9dca7d2-cef9-4cef-a47a-8cc30613ba63 | |
+ if (!cartPricingAbortController.current) { | |
+ cartPricingAbortController.current = new AbortController(); | |
+ } else { | |
+ cartPricingAbortController.current.abort(); // Abort previous request | |
+ cartPricingAbortController.current = new AbortController(); | |
+ } | |
+ | |
+ const signal = cartPricingAbortController.current.signal; | |
+ | |
+ for (const lineItem of itemDetailz.values()) { | |
+ if (lineItem.item.upc === "NEW ITEM") { | |
+ const unknownItem: UnknownItem = { | |
+ upc: lineItem.item.upc, | |
+ name: lineItem.item.name, | |
+ quantity: lineItem.quantity, | |
+ price: lineItem.item.sellPrice, | |
+ bottleDepositMultiplier: lineItem.item.bottleFeeMultiplier, | |
+ envFeeMultiplier: lineItem.item.envFeeMultiplier, | |
+ department: lineItem.item.department.name, | |
+ }; | |
+ pricingCartInputItems.push(unknownItem); | |
+ } else { | |
+ let discountOff: number | undefined = undefined; | |
+ if (discount > 0) { | |
+ discountOff = discount / 100; | |
+ } else if (lineItem.discountOff !== undefined) { | |
+ discountOff = lineItem.discountOff; | |
+ } | |
+ let taxExempted: boolean | undefined = taxExempt; | |
+ if (lineItem.taxExempt !== undefined) { | |
+ taxExempted = lineItem.taxExempt; | |
+ } | |
+ const pricingCartInputItem: PricingCartInputItem = { | |
+ itemId: lineItem.item.id, | |
+ quantity: lineItem.quantity, | |
+ discountOff: discountOff, | |
+ taxExempt: taxExempted, | |
+ }; | |
+ | |
+ pricingCartInputItems.push(pricingCartInputItem); | |
+ } | |
+ } | |
+ | |
+ const getCartPricing = async () => { | |
+ try { | |
+ const response = await cartApi.getCartPricing( | |
+ { | |
+ items: pricingCartInputItems, | |
+ zoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, | |
+ }, | |
+ { | |
+ signal: signal, | |
+ }, | |
+ ); | |
+ const newItemDetails = new Map( | |
+ response.data.map((i) => { | |
+ const lineItem: LineItem = { | |
+ ...i, | |
+ quantityAsString: i.quantity.toString(), | |
+ totalPrice: i.total, | |
+ promotionAmounts: i.promotionAmounts, | |
+ item: { | |
+ ...i.item, // should always be on top | |
+ ...i, | |
+ id: i.id ?? Math.floor(Math.random() * 900000), | |
+ bottleFeeMultiplier: i.bottleDepositMultiplier, | |
+ sellPrice: i.price, | |
+ isDiscountAllowed: i.isDiscountAllowed ?? false, | |
+ onHandQuantity: i.quantity, | |
+ }, | |
+ }; | |
+ return [lineItem.item.id, lineItem]; | |
+ }), | |
+ ); | |
+ | |
+ setItems(newItemDetails); | |
+ setItemsInCart(newItemDetails); | |
+ setDiscount(0); | |
+ } catch (err) { | |
+ Sentry.captureException(err); | |
+ console.error(err); | |
+ } | |
+ }; | |
+ getCartPricing(); | |
+ | |
+ // Clean up | |
+ return () => { | |
+ cartPricingAbortController.current?.abort(); | |
+ }; | |
+ }, | |
+ [discount, setItems, taxExempt, entity?.id, cartApi, setItemsInCart], | |
+ ); | |
+ | |
+ const addSingleItemToCart = useCallback( | |
+ (itemDetailz: Item, quantity = 1) => { | |
+ setItems((prev) => { | |
+ const m = new Map(prev); | |
+ const lineItem = addSingleItem(itemDetailz, m, quantity, taxExempt); | |
+ prev.set(itemDetailz.id, lineItem); | |
+ | |
+ poleDisplay( | |
+ `${lineItem.quantity} ${lineItem.item.name | |
+ .trim() | |
+ .substring(0, 9)} $${lineItem.item.sellPrice.toFixed(2)}`, | |
+ `Grand Total $${transactionTotalPrice().toFixed(2)}`, | |
+ ); | |
+ m.set(itemDetailz.id, lineItem); | |
+ calculateCartPrice(m); | |
+ return m; | |
+ }); | |
+ setQuantity(1); | |
+ }, | |
+ [calculateCartPrice, poleDisplay, taxExempt, transactionTotalPrice], | |
+ ); | |
+ | |
+ useEffect(() => { | |
+ calculateCartPrice(items); | |
+ }, [ | |
+ taxExempt, | |
+ // only need to recalculate when taxExempt changes | |
+ calculateCartPrice, | |
+ // items, | |
+ ]); | |
+ | |
+ useEffect(() => { | |
+ calculateCartPrice(items); | |
+ }, [ | |
+ discount, | |
+ // only need to recalculate when discount changes | |
+ calculateCartPrice, | |
+ // items, | |
+ ]); | |
+ | |
+ const retrieveItemsLikeUpc = useCallback( | |
+ async (barCode?: string, quantity = 1) => { | |
+ // AbortController setup | |
+ // https://chat.openai.com/share/c9dca7d2-cef9-4cef-a47a-8cc30613ba63 | |
+ | |
+ const signal = retrieveUpcAbortController.current.signal; | |
+ | |
+ if (!entity?.id) return; | |
+ if (barCode && api) { | |
+ setQuantity(1); | |
+ try { | |
+ const res = await api.retrieveItemsLikeUpc( | |
+ { | |
+ upcCode: `%${barCode}%`, | |
+ entityId: entity.id, | |
+ }, | |
+ { | |
+ signal: signal, | |
+ }, | |
+ ); | |
+ | |
+ if (res.data.length === 1) { | |
+ addSingleItemToCart(res.data[0], quantity); | |
+ } else if (res.data.length === 0) { | |
+ setMultipleChoices({ items: [], quantity: quantity }); | |
+ } else { | |
+ setMultipleChoices({ items: res.data, quantity: quantity }); | |
+ } | |
+ } catch (err: any) { | |
+ Sentry.captureException(err); | |
+ console.error(err.message); | |
+ } | |
+ } | |
+ }, | |
+ [api, entity, addSingleItemToCart], | |
); | |
const deleteAll = useCallback(() => { | |
setItems(new Map()); | |
setTaxExempt(false); | |
setGovernmentID(undefined); | |
+ setDiscount(0); | |
+ retrieveUpcAbortController.current.abort(); | |
+ retrieveUpcAbortController.current = new AbortController(); | |
+ cartPricingAbortController.current?.abort(); | |
+ cartPricingAbortController.current = new AbortController(); | |
}, []); | |
+ if (isEntityLoading) return <Loading />; | |
+ if (!cartApi) return <Loading />; | |
+ if (!api) return <Loading />; | |
+ | |
return ( | |
<SalesContext.Provider | |
value={{ | |
items, | |
setItems, | |
+ calculateCartPrice, | |
+ addSingleItemToCart, | |
quantity, | |
setQuantity, | |
taxExempt, | |
diff --git a/src/context/Sales/SalesPaymentContext.tsx b/src/context/Sales/SalesPaymentContext.tsx | |
index 53c4fb7..4e283a5 100644 | |
--- a/src/context/Sales/SalesPaymentContext.tsx | |
+++ b/src/context/Sales/SalesPaymentContext.tsx | |
@@ -1,4 +1,5 @@ | |
import { useDisclosure, UseDisclosureReturn, useToast } from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { AxiosResponse } from "axios"; | |
import _ from "lodash"; | |
import { | |
@@ -270,6 +271,7 @@ export const SalesPaymentProvider = ( | |
kachingSound.play(); | |
} catch (e) { | |
console.warn("Kaching Failed to play:", e); | |
+ Sentry.captureException(e); | |
} | |
const changeDue = calculateChangeDue(amountReceived); | |
toast({ | |
@@ -317,6 +319,7 @@ export const SalesPaymentProvider = ( | |
console.error(err); | |
} | |
} | |
+ Sentry.captureException(err); | |
setIsPersisting(false); | |
} | |
} | |
diff --git a/src/hooks/useAllDepartments.tsx b/src/hooks/useAllDepartments.tsx | |
index 6070172..c425260 100644 | |
--- a/src/hooks/useAllDepartments.tsx | |
+++ b/src/hooks/useAllDepartments.tsx | |
@@ -1,3 +1,4 @@ | |
+import * as Sentry from "@sentry/react"; | |
import { useEffect, useState } from "react"; | |
import { useDepartmentApi } from "../config/ItemsApi"; | |
import { Department } from "../model/data-contracts"; | |
@@ -10,22 +11,26 @@ export const useAllDepartments = () => { | |
> | null>(); | |
useEffect(() => { | |
- departmentApi | |
- ?.getDepartment() | |
- .then((res) => { | |
- setDepartments( | |
- new Map( | |
- res.data.departments.map((department) => [ | |
- department.name, | |
- department, | |
- ]), | |
- ), | |
- ); | |
- }) | |
- .catch((e) => { | |
+ const fetchDepartments = async () => { | |
+ try { | |
+ const response = await departmentApi?.getDepartment(); | |
+ if (response) { | |
+ setDepartments( | |
+ new Map( | |
+ response.data.departments.map((department) => [ | |
+ department.name, | |
+ department, | |
+ ]), | |
+ ), | |
+ ); | |
+ } | |
+ } catch (e) { | |
console.error(e); | |
setDepartments(null); | |
- }); | |
+ Sentry.captureException(e); | |
+ } | |
+ }; | |
+ fetchDepartments(); | |
}, [departmentApi]); | |
return departments; | |
diff --git a/src/hooks/useAllVendors.tsx b/src/hooks/useAllVendors.tsx | |
index 3b14a63..6ec9444 100644 | |
--- a/src/hooks/useAllVendors.tsx | |
+++ b/src/hooks/useAllVendors.tsx | |
@@ -1,3 +1,4 @@ | |
+import * as Sentry from "@sentry/react"; | |
import { useEffect, useState } from "react"; | |
import { useVendorApi } from "../config/ItemsApi"; | |
import { Vendor } from "../model/data-contracts"; | |
@@ -7,17 +8,23 @@ export const useAllVendors = () => { | |
const [vendors, setVendors] = useState<Map<Vendor["name"], Vendor> | null>(); | |
useEffect(() => { | |
- vendorApi | |
- ?.getVendor() | |
- .then((res) => { | |
- setVendors( | |
- new Map(res.data.vendors.map((vendor) => [vendor.name, vendor])), | |
- ); | |
- }) | |
- .catch((e) => { | |
+ const fetchVendors = async () => { | |
+ try { | |
+ const response = await vendorApi?.getVendor(); | |
+ if (response) { | |
+ setVendors( | |
+ new Map( | |
+ response.data.vendors.map((vendor) => [vendor.name, vendor]), | |
+ ), | |
+ ); | |
+ } | |
+ } catch (e) { | |
console.error(e); | |
setVendors(null); | |
- }); | |
+ Sentry.captureException(e); | |
+ } | |
+ }; | |
+ fetchVendors(); | |
}, [vendorApi]); | |
return vendors; | |
diff --git a/src/hooks/useURLPageState/useURLPageState.tsx b/src/hooks/useURLPageState/useURLPageState.tsx | |
index 42998b9..1c6229a 100644 | |
--- a/src/hooks/useURLPageState/useURLPageState.tsx | |
+++ b/src/hooks/useURLPageState/useURLPageState.tsx | |
@@ -5,12 +5,15 @@ import { PageMetadata } from "../../model/data-contracts"; | |
import { ColumnSortParam } from "./ColumnSortParam"; | |
export const useURLPageState = (initialParams?: { | |
- page?: number; | |
size?: number; | |
sort?: ColumnSort[]; | |
}) => { | |
+ const [urlPageNum, setPageNumber] = useQueryParam( | |
+ "page", | |
+ withDefault(NumberParam, 1), | |
+ ); | |
const [pageState, setPageState] = useState<PageMetadata>({ | |
- number: initialParams?.page ?? 1, | |
+ number: urlPageNum ?? 1, | |
totalPages: 1, | |
size: initialParams?.size ?? 20, | |
totalElements: 0, | |
@@ -19,7 +22,6 @@ export const useURLPageState = (initialParams?: { | |
initialParams?.sort ?? [], | |
); | |
- const [, setPageNumber] = useQueryParam("page", withDefault(NumberParam, 1)); | |
const [, setSortingURLState] = useQueryParam( | |
"sort", | |
withDefault(ColumnSortParam, initialParams?.sort ?? []), | |
diff --git a/src/model/Device.ts b/src/model/Device.ts | |
new file mode 100644 | |
index 0000000..0e41b40 | |
--- /dev/null | |
+++ b/src/model/Device.ts | |
@@ -0,0 +1,46 @@ | |
+/* eslint-disable */ | |
+/* tslint:disable */ | |
+/* | |
+ * --------------------------------------------------------------- | |
+ * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## | |
+ * ## ## | |
+ * ## AUTHOR: acacode ## | |
+ * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## | |
+ * --------------------------------------------------------------- | |
+ */ | |
+ | |
+import { EntityId, Error, RegisterId } from "./data-contracts"; | |
+import { HttpClient, RequestParams } from "./http-client"; | |
+ | |
+export class Device<SecurityDataType = unknown> { | |
+ http: HttpClient<SecurityDataType>; | |
+ | |
+ constructor(http: HttpClient<SecurityDataType>) { | |
+ this.http = http; | |
+ } | |
+ | |
+ /** | |
+ * @description Cancel an ongoing transaction on device | |
+ * | |
+ * @name CancelPayin | |
+ * @summary Cancel an ongoing transaction on device | |
+ * @request POST:/device/cancelPayin | |
+ * @secure | |
+ */ | |
+ cancelPayin = ( | |
+ query: { | |
+ /** The unique identifier of an entity */ | |
+ entityId: EntityId; | |
+ /** The register number */ | |
+ registerNumber: RegisterId; | |
+ }, | |
+ params: RequestParams = {}, | |
+ ) => | |
+ this.http.request<void, Error | void>({ | |
+ path: `/device/cancelPayin`, | |
+ method: "POST", | |
+ query: query, | |
+ secure: true, | |
+ ...params, | |
+ }); | |
+} | |
diff --git a/src/model/Employees.ts b/src/model/Employees.ts | |
index f173902..c318e6b 100644 | |
--- a/src/model/Employees.ts | |
+++ b/src/model/Employees.ts | |
@@ -51,8 +51,7 @@ export class Employees<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get punch in/out history and overview for the employee | |
* | |
* @tags readonly, employee | |
@@ -76,8 +75,7 @@ export class Employees<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* No description | |
* | |
* @name TimeAllDetail | |
@@ -100,8 +98,7 @@ export class Employees<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* No description | |
* | |
* @name TimePunchCreate | |
diff --git a/src/model/Entity.ts b/src/model/Entity.ts | |
index b4bb315..2ea281d 100644 | |
--- a/src/model/Entity.ts | |
+++ b/src/model/Entity.ts | |
@@ -55,8 +55,7 @@ export class Entity<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Entity by ID | |
* | |
* @tags readonly, entity | |
@@ -71,8 +70,7 @@ export class Entity<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Users by Entity | |
* | |
* @tags readonly, entity, user | |
@@ -98,8 +96,7 @@ export class Entity<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Add User to Entity | |
* | |
* @tags mutative, entity, user | |
@@ -124,8 +121,7 @@ export class Entity<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Edit UserEntity relationship, Currently only used for changing the role of a user for an entity. | |
* | |
* @tags mutative, entity, user, role | |
@@ -151,8 +147,7 @@ export class Entity<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Delete User from Entity | |
* | |
* @tags mutative, entity, user | |
@@ -174,8 +169,7 @@ export class Entity<SecurityDataType = unknown> { | |
query: query, | |
secure: true, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Roles by Entity | |
* | |
* @tags readonly, entity, role | |
@@ -190,8 +184,7 @@ export class Entity<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Edit Permissions by Entity | |
* | |
* @tags mutative, entity, role | |
@@ -212,8 +205,7 @@ export class Entity<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Events by Entity | |
* | |
* @tags readonly, entity, event | |
@@ -261,8 +253,7 @@ export class Entity<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get all tags | |
* | |
* @tags readonly, tag | |
@@ -294,8 +285,7 @@ export class Entity<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Create a tag | |
* | |
* @tags mutative, tag | |
@@ -316,8 +306,7 @@ export class Entity<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Patch a tag | |
* | |
* @tags mutative, tag | |
@@ -339,8 +328,7 @@ export class Entity<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Delete a tag | |
* | |
* @tags mutative, tag | |
diff --git a/src/model/InvoiceApi.ts b/src/model/InvoiceApi.ts | |
index 85ee4b5..06e32a2 100644 | |
--- a/src/model/InvoiceApi.ts | |
+++ b/src/model/InvoiceApi.ts | |
@@ -67,8 +67,7 @@ export class InvoiceApi<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Invoice Tree by Pk | |
* | |
* @tags readonly, invoice | |
@@ -82,8 +81,21 @@ export class InvoiceApi<SecurityDataType = unknown> { | |
method: "GET", | |
secure: true, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
+ * @description Delete invoice | |
+ * | |
+ * @tags mutative, invoice | |
+ * @name InvoiceApiDelete | |
+ * @request DELETE:/invoiceApi/{id} | |
+ * @secure | |
+ */ | |
+ invoiceApiDelete = (id: InvoiceId, params: RequestParams = {}) => | |
+ this.http.request<void, any>({ | |
+ path: `/invoiceApi/${id}`, | |
+ method: "DELETE", | |
+ secure: true, | |
+ ...params, | |
+ }); /** | |
* @description Create an Invoice | |
* | |
* @tags mutative, invoice | |
@@ -99,8 +111,7 @@ export class InvoiceApi<SecurityDataType = unknown> { | |
secure: true, | |
type: ContentType.Json, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Patch an Invoice | |
* | |
* @tags mutative, invoice | |
@@ -116,8 +127,7 @@ export class InvoiceApi<SecurityDataType = unknown> { | |
secure: true, | |
type: ContentType.Json, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get the cost of an invoice grouped by vendor within a certain time range. | |
* | |
* @tags readonly, invoice | |
@@ -142,8 +152,7 @@ export class InvoiceApi<SecurityDataType = unknown> { | |
query: query, | |
secure: true, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Create or Add Item to Invoice | |
* | |
* @tags readonly, invoice | |
diff --git a/src/model/Items.ts b/src/model/Items.ts | |
index c1b837b..91f34e9 100644 | |
--- a/src/model/Items.ts | |
+++ b/src/model/Items.ts | |
@@ -71,8 +71,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get a list of items | |
* | |
* @name GetItems | |
@@ -94,8 +93,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Search for items | |
* | |
* @tags readonly, item | |
@@ -134,8 +132,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Search for items | |
* | |
* @tags readonly, item | |
@@ -172,8 +169,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get details for an item | |
* | |
* @tags readonly, item | |
@@ -189,8 +185,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* No description | |
* | |
* @name DetailsCreate | |
@@ -211,8 +206,7 @@ export class Items<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Update Item | |
* | |
* @tags item, mutative | |
@@ -234,8 +228,7 @@ export class Items<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Delete an Item. Soft Delete | |
* | |
* @tags item, mutative | |
@@ -251,8 +244,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get details for an item | |
* | |
* @tags readonly, item | |
@@ -277,8 +269,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get State Min Violations for months and years given | |
* | |
* @tags readonly, item | |
@@ -311,8 +302,7 @@ export class Items<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Obtains the items not sold within a time period. | |
* | |
* @tags readonly | |
@@ -346,8 +336,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get a sales report by item | |
* | |
* @tags readonly, item, report | |
@@ -382,8 +371,7 @@ export class Items<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get the sale history for an item at a specified interval | |
* | |
* @tags readonly, item | |
diff --git a/src/model/PromotionApi.ts b/src/model/PromotionApi.ts | |
index c269d32..65e724c 100644 | |
--- a/src/model/PromotionApi.ts | |
+++ b/src/model/PromotionApi.ts | |
@@ -68,8 +68,7 @@ export class PromotionApi<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description List Promotions | |
* | |
* @tags readonly, promotion | |
@@ -99,6 +98,8 @@ export class PromotionApi<SecurityDataType = unknown> { | |
endDate?: string; | |
/** Promotion is active, inactive, upcoming. pass null for all */ | |
status?: PromotionStatus; | |
+ /** https://github.com/eggert/tz/blob/main/backward */ | |
+ zoneId: string; | |
page?: number; | |
sort?: Array<string>; | |
size?: number; | |
@@ -119,8 +120,7 @@ export class PromotionApi<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Promotion by Pk | |
* | |
* @tags readonly, promotion | |
@@ -134,8 +134,7 @@ export class PromotionApi<SecurityDataType = unknown> { | |
method: "GET", | |
secure: true, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Delete promotion | |
* | |
* @tags promotion, mutative | |
@@ -149,8 +148,7 @@ export class PromotionApi<SecurityDataType = unknown> { | |
method: "DELETE", | |
secure: true, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Create an Promotion | |
* | |
* @tags mutative, promotion | |
@@ -166,8 +164,7 @@ export class PromotionApi<SecurityDataType = unknown> { | |
secure: true, | |
type: ContentType.Json, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Patch a Promotion and its items | |
* | |
* @tags mutative, promotion | |
diff --git a/src/model/Purchaseorder.ts b/src/model/Purchaseorder.ts | |
index fec802f..ebbbf63 100644 | |
--- a/src/model/Purchaseorder.ts | |
+++ b/src/model/Purchaseorder.ts | |
@@ -56,8 +56,7 @@ export class Purchaseorder<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Create a purchase order | |
* | |
* @tags mutative, purchase_order | |
@@ -83,8 +82,7 @@ export class Purchaseorder<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get details for a PO | |
* | |
* @name PurchaseorderDetail | |
@@ -99,8 +97,7 @@ export class Purchaseorder<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description update the details of an item in a PO | |
* | |
* @tags mutative, purchase_order | |
diff --git a/src/model/Transaction.ts b/src/model/Transaction.ts | |
index 3c54aba..2e24fa8 100644 | |
--- a/src/model/Transaction.ts | |
+++ b/src/model/Transaction.ts | |
@@ -58,8 +58,7 @@ export class Transaction<SecurityDataType = unknown> { | |
secure: true, | |
type: ContentType.Json, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get items to the cart for a register. | |
* | |
* @tags readonly, transaction | |
@@ -83,8 +82,7 @@ export class Transaction<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Place items to the cart for a register. | |
* | |
* @tags mutative, transaction | |
@@ -100,8 +98,7 @@ export class Transaction<SecurityDataType = unknown> { | |
secure: true, | |
type: ContentType.Json, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Deletes item from cart if itemId is given otherwise empties all items from the cart for a register. | |
* | |
* @tags mutative, transaction | |
@@ -126,8 +123,7 @@ export class Transaction<SecurityDataType = unknown> { | |
query: query, | |
secure: true, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Gets a single transaction | |
* | |
* @tags readonly, transaction | |
@@ -143,8 +139,7 @@ export class Transaction<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Voids a single transaction | |
* | |
* @tags mutative, transaction | |
@@ -168,8 +163,7 @@ export class Transaction<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Delete a single transaction that is cash only | |
* | |
* @tags mutative, transaction | |
diff --git a/src/model/Transactions.ts b/src/model/Transactions.ts | |
index d655fb5..5ee7183 100644 | |
--- a/src/model/Transactions.ts | |
+++ b/src/model/Transactions.ts | |
@@ -57,8 +57,7 @@ export class Transactions<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Closes out transactions for current register | |
* | |
* @tags mutative, transaction | |
@@ -79,8 +78,7 @@ export class Transactions<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get past transactions | |
* | |
* @tags readonly, transaction | |
@@ -111,8 +109,7 @@ export class Transactions<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Save a transaction | |
* | |
* @tags mutative, transaction | |
@@ -130,8 +127,7 @@ export class Transactions<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Patch a transaction | |
* | |
* @tags mutative, transaction | |
@@ -152,8 +148,7 @@ export class Transactions<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get transactions report broken down by department | |
* | |
* @tags readonly, transaction | |
@@ -186,8 +181,7 @@ export class Transactions<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get transactions report broken down by date | |
* | |
* @tags transaction, readonly | |
diff --git a/src/model/User.ts b/src/model/User.ts | |
index 63b8095..c43ed83 100644 | |
--- a/src/model/User.ts | |
+++ b/src/model/User.ts | |
@@ -39,8 +39,7 @@ export class User<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Edit User | |
* | |
* @tags mutative, user | |
@@ -57,8 +56,7 @@ export class User<SecurityDataType = unknown> { | |
type: ContentType.Json, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Authorize User | |
* | |
* @tags readonly, user | |
@@ -73,8 +71,7 @@ export class User<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Authorize Employee | |
* | |
* @tags readonly, user | |
diff --git a/src/model/Vendor.ts b/src/model/Vendor.ts | |
index 6912ee7..17fa313 100644 | |
--- a/src/model/Vendor.ts | |
+++ b/src/model/Vendor.ts | |
@@ -40,8 +40,7 @@ export class Vendor<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* @description Get Matching Vendor Items | |
* | |
* @name ItemList | |
@@ -63,8 +62,7 @@ export class Vendor<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* No description | |
* | |
* @name ItemsList | |
@@ -86,8 +84,7 @@ export class Vendor<SecurityDataType = unknown> { | |
secure: true, | |
format: "json", | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* No description | |
* | |
* @name ItemMappingCreate | |
@@ -106,8 +103,7 @@ export class Vendor<SecurityDataType = unknown> { | |
secure: true, | |
type: ContentType.Json, | |
...params, | |
- }); | |
- /** | |
+ }); /** | |
* No description | |
* | |
* @name ItemMappingDelete | |
diff --git a/src/model/data-contracts.ts b/src/model/data-contracts.ts | |
index 6d742a3..036e312 100644 | |
--- a/src/model/data-contracts.ts | |
+++ b/src/model/data-contracts.ts | |
@@ -382,7 +382,8 @@ export type TransactionCartItem = TransactionData & | |
}; | |
export interface TransactionsClosingInput { | |
- registerNumber: number; | |
+ /** A component for the identifier of a register */ | |
+ registerNumber: RegisterId; | |
/** The unique identifier of an employee */ | |
employeeId: EmployeeId; | |
/** | |
@@ -839,7 +840,7 @@ export interface RegisterPk { | |
export type Register = RegisterPk & { | |
creditCardDeviceId?: string; | |
- lastTransactionClosing?: TransactionsClosing; | |
+ lastTransactionClosingId?: number; | |
}; | |
export interface CustomerPk { | |
@@ -1205,9 +1206,6 @@ export interface PricingCartInputItem { | |
/** The unique identifier of an item */ | |
itemId: ItemId; | |
quantity: PromotionQuantity; | |
-} | |
- | |
-export interface PricingCartInput { | |
/** | |
* Discount off of 0.40 would mean 40% off i.e. 60% on | |
* @min 0 | |
@@ -1216,12 +1214,12 @@ export interface PricingCartInput { | |
discountOff?: number; | |
/** @default false */ | |
taxExempt?: boolean; | |
+} | |
+ | |
+export interface PricingCartInput { | |
+ zoneId: string; | |
/** @default [] */ | |
- items?: ( | |
- | PricingCartInputItem | |
- | UnknownItem | |
- | (PricingCartInputItem & UnknownItem) | |
- )[]; | |
+ items?: (PricingCartInputItem | UnknownItem)[]; | |
} | |
export interface PromotionAmount { | |
@@ -1235,6 +1233,14 @@ export interface PricingCartOutputItem { | |
id?: ItemId; | |
/** The bar code for an item */ | |
upc: ItemUpc; | |
+ /** | |
+ * Discount off of 0.40 would mean 40% off i.e. 60% on | |
+ * @min 0 | |
+ * @max 1 | |
+ */ | |
+ discountOff?: number; | |
+ /** @default false */ | |
+ taxExempt?: boolean; | |
/** Name of item */ | |
name: ItemName; | |
price: ItemSellPrice; | |
diff --git a/src/pages/Auth/AuthorizationGuard.tsx b/src/pages/Auth/AuthorizationGuard.tsx | |
index 6d49c37..252b3fc 100644 | |
--- a/src/pages/Auth/AuthorizationGuard.tsx | |
+++ b/src/pages/Auth/AuthorizationGuard.tsx | |
@@ -54,7 +54,7 @@ export const AuthorizationGuard: React.FC<AuthorizationFlowProps> = () => { | |
} | |
if (authorizedUser?.needsSetup) { | |
- navigate("/profile", { state: { onComplete: "/sale" } }); | |
+ navigate("/profile", { state: { onComplete: "/sale/" } }); | |
} | |
if (entity && !isStoreLogin() && user && permissions) { | |
diff --git a/src/pages/Auth/SignIn.tsx b/src/pages/Auth/SignIn.tsx | |
index 16750f0..a8dfb83 100644 | |
--- a/src/pages/Auth/SignIn.tsx | |
+++ b/src/pages/Auth/SignIn.tsx | |
@@ -1,5 +1,4 @@ | |
import { useState } from "react"; | |
-import { useNavigate } from "react-router-dom"; | |
import { AuthenticationCard } from "../../components/Auth/AuthenticationCard/AuthenticationCard"; | |
import { AuthenticationEmailSignInForm } from "../../components/Auth/AuthenticationCard/AuthenticationEmailSignInForm"; | |
import { CenteredLayout } from "../../components/layouts/CenteredLayout"; | |
@@ -8,7 +7,6 @@ import { Logo } from "../../Logo"; | |
export const SignIn = () => { | |
const [error, setError] = useState(""); | |
- const navigate = useNavigate(); | |
const { signIn, triggerResetEmail, googleSignIn } = useUserAuth(); | |
const handleSignIn = async ( | |
diff --git a/src/pages/Auth/UnauthorizedPage.tsx b/src/pages/Auth/UnauthorizedPage.tsx | |
index 9aed4b8..3729c0a 100644 | |
--- a/src/pages/Auth/UnauthorizedPage.tsx | |
+++ b/src/pages/Auth/UnauthorizedPage.tsx | |
@@ -4,21 +4,25 @@ import { NavBarPlain } from "../../components/navbar/NavBarPlain"; | |
export type UnauthorizedPageProps = { | |
overrideAllowed?: boolean; | |
+ showNavbar?: boolean; | |
+ showBackButton?: boolean; | |
message?: string; | |
children?: React.ReactNode; | |
}; | |
export const UnauthorizedPage: React.FC<UnauthorizedPageProps> = ({ | |
overrideAllowed = true, | |
+ showNavbar = true, | |
+ showBackButton = true, | |
message = "Please contact your administrator to request access.", | |
children, | |
}) => { | |
return ( | |
<> | |
- <NavBarPlain /> | |
+ {showNavbar && <NavBarPlain />} | |
<CenteredLayout> | |
<UnauthorizedCard | |
- showBackButton | |
+ showBackButton={showBackButton} | |
overrideAllowed={overrideAllowed} | |
message={message} | |
> | |
diff --git a/src/pages/CreateItem.tsx b/src/pages/CreateItem.tsx | |
index c54416f..fceb016 100644 | |
--- a/src/pages/CreateItem.tsx | |
+++ b/src/pages/CreateItem.tsx | |
@@ -29,6 +29,7 @@ import { | |
useToast, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCounter } from "@uidotdev/usehooks"; | |
import { Field, Form, Formik } from "formik"; | |
import { useContext, useEffect, useState } from "react"; | |
@@ -203,20 +204,21 @@ export const CreateItemForm = ({ | |
return; | |
} | |
- api | |
- ?.detailsCreate( | |
- 1, // overridden by server | |
- { | |
- ...values, | |
- sellPrice, | |
- department: departments.get(values.department!)!, | |
- vendorMapping, | |
- tags: values.tags.map((tag) => tag.name), | |
- entity: entity, | |
- id: values.id ?? 0, | |
- }, | |
- ) | |
- .then((res) => { | |
+ const createItem = async () => { | |
+ if (!api) return; | |
+ try { | |
+ const res = await api.detailsCreate( | |
+ 1, // overridden by server | |
+ { | |
+ ...values, | |
+ sellPrice, | |
+ department: departments.get(values.department!)!, | |
+ vendorMapping, | |
+ tags: values.tags.map((tag) => tag.name), | |
+ entity: entity, | |
+ id: values.id ?? 0, | |
+ }, | |
+ ); | |
onSubmit(res.data); | |
setLastCreatedItem(res.data); | |
toast({ | |
@@ -227,17 +229,18 @@ export const CreateItemForm = ({ | |
}); | |
setUpc(""); | |
formKeyActions.increment(); | |
- }) | |
- .catch(() => { | |
+ } catch (err) { | |
toast({ | |
title: "Item failed to create. Please retry", | |
status: "error", | |
isClosable: true, | |
}); | |
- }) | |
- .finally(() => { | |
+ Sentry.captureException(err); | |
+ } finally { | |
actions.setSubmitting(false); | |
- }); | |
+ } | |
+ }; | |
+ createItem(); | |
}} | |
validationSchema={CreateItemSchema} | |
> | |
diff --git a/src/pages/EmployeeTimeClock.tsx b/src/pages/EmployeeTimeClock.tsx | |
index 99b97d2..96a6eed 100644 | |
--- a/src/pages/EmployeeTimeClock.tsx | |
+++ b/src/pages/EmployeeTimeClock.tsx | |
@@ -18,6 +18,7 @@ import { | |
Thead, | |
Tr, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useEffect, useState } from "react"; | |
import { useNavigate } from "react-router-dom"; | |
import { AdminTimeClock } from "../components/Employee/AdminTimeClock"; | |
@@ -68,12 +69,18 @@ export const EmployeeTimeClock = () => { | |
useEffect(() => { | |
if (!entity?.id) return; | |
if (!employeeApi) return; | |
- employeeApi | |
- .getEmployees({ entityId: entity.id }) | |
- .then((employees) => { | |
+ const fetchEmployees = async () => { | |
+ try { | |
+ const employees = await employeeApi.getEmployees({ | |
+ entityId: entity.id, | |
+ }); | |
setEmployees(employees.data.employees); | |
- }) | |
- .catch((error) => console.log(error)); | |
+ } catch (error) { | |
+ console.log(error); | |
+ Sentry.captureException(error); | |
+ } | |
+ }; | |
+ fetchEmployees(); | |
}, [employeeApi, entity?.id]); | |
const handleEmployeeSelection = useCallback( | |
@@ -104,6 +111,7 @@ export const EmployeeTimeClock = () => { | |
} catch (error) { | |
console.log(error); | |
setPin(""); | |
+ Sentry.captureException(error); | |
} | |
} | |
}, | |
@@ -116,51 +124,60 @@ export const EmployeeTimeClock = () => { | |
setLoggedOn(false); | |
}, []); | |
- const handlePunch2 = () => { | |
+ const handlePunch2 = async () => { | |
if (employeeApi && selectedEmployee && pin) { | |
- employeeApi | |
- .timePunchCreate(selectedEmployee.id, { | |
+ try { | |
+ const resp = await employeeApi.timePunchCreate(selectedEmployee.id, { | |
employeePin: pin, | |
- }) | |
- .then((resp) => { | |
- if (employeeTimeRecords) { | |
- let index: number = employeeTimeRecords.records.findIndex( | |
- (record) => record.id === resp.data.id, | |
- ); | |
+ }); | |
+ | |
+ if (employeeTimeRecords) { | |
+ const index: number = employeeTimeRecords.records.findIndex( | |
+ (record) => record.id === resp.data.id, | |
+ ); | |
- const employeeTimeRecordsUpdated = { ...employeeTimeRecords }; | |
- if (index >= 0) { | |
- employeeTimeRecordsUpdated.records = | |
- employeeTimeRecords.records.map((record) => | |
- record.id === resp.data.id ? resp.data : record, | |
- ); | |
- } else { | |
- employeeTimeRecordsUpdated.records = [ | |
- resp.data, | |
- ...employeeTimeRecords.records, | |
- ]; | |
- } | |
- setEmployeeTimeRecords(employeeTimeRecordsUpdated); | |
+ const employeeTimeRecordsUpdated = { ...employeeTimeRecords }; | |
+ if (index >= 0) { | |
+ employeeTimeRecordsUpdated.records = | |
+ employeeTimeRecords.records.map((record) => | |
+ record.id === resp.data.id ? resp.data : record, | |
+ ); | |
+ } else { | |
+ employeeTimeRecordsUpdated.records = [ | |
+ resp.data, | |
+ ...employeeTimeRecords.records, | |
+ ]; | |
} | |
- }) | |
- .catch((error) => console.log(error)); | |
+ setEmployeeTimeRecords(employeeTimeRecordsUpdated); | |
+ } | |
+ } catch (error) { | |
+ console.log(error); | |
+ Sentry.captureException(error); | |
+ } | |
} | |
}; | |
- const handlePunch = (record: EmployeeTimeRecord) => { | |
+ const handlePunch = async (record: EmployeeTimeRecord) => { | |
if (employeeApi && selectedEmployee && pin) { | |
- employeeApi | |
- .timePunchCreate(selectedEmployee.id, { | |
+ try { | |
+ await employeeApi.timePunchCreate(selectedEmployee.id, { | |
employeePin: pin, | |
recordId: record.id, | |
- }) | |
- .then(() => { | |
- navigate(0); | |
- }) | |
- .catch((error) => console.log(error)); | |
+ }); | |
+ navigate(0); | |
+ } catch (error) { | |
+ console.log(error); | |
+ Sentry.captureException(error); | |
+ } | |
} | |
}; | |
+ useEffect(() => { | |
+ if (pin.length === 4) { | |
+ handleLogOn(pin); | |
+ } | |
+ }, [pin]); | |
+ | |
return ( | |
<Stack | |
background={"gray.100"} | |
@@ -347,7 +364,13 @@ export const EmployeeTimeClock = () => { | |
</Select> | |
{selectedEmployee && ( | |
<HStack width="full" justify="space-between"> | |
- <PinInput size="lg" mask onChange={setPin} value={pin}> | |
+ <PinInput | |
+ size="lg" | |
+ mask | |
+ onChange={setPin} | |
+ value={pin} | |
+ autoFocus={true} | |
+ > | |
<PinInputField autoComplete="off" /> | |
<PinInputField autoComplete="off" /> | |
<PinInputField autoComplete="off" /> | |
diff --git a/src/pages/ItemDetailPage.tsx b/src/pages/ItemDetailPage.tsx | |
index a9abdb7..b85cd04 100644 | |
--- a/src/pages/ItemDetailPage.tsx | |
+++ b/src/pages/ItemDetailPage.tsx | |
@@ -34,6 +34,7 @@ import { | |
useToast, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useEffect, useRef, useState } from "react"; | |
import { FaTrash } from "react-icons/fa"; | |
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; | |
@@ -129,16 +130,18 @@ export const ItemDetailPage = () => { | |
// When page is loaded, fetch the item details | |
useEffect(() => { | |
if (id && api) { | |
- setItemDetails(undefined); | |
- api | |
- .detailsDetail(+id) | |
- .then((res) => { | |
+ const fetchItemDetails = async () => { | |
+ try { | |
+ const res = await api.detailsDetail(+id); | |
setItemDetails(res.data); | |
setItemDetailsApi(res.data); | |
- }) | |
- .catch((err) => { | |
+ } catch (err: any) { | |
+ Sentry.captureException(err); | |
console.error(err.message); | |
- }); | |
+ } | |
+ }; | |
+ fetchItemDetails(); | |
+ setItemDetails(undefined); | |
} | |
}, [id, api]); | |
diff --git a/src/pages/ItemSearchPage.tsx b/src/pages/ItemSearchPage.tsx | |
index 90bb3f6..a3484f7 100644 | |
--- a/src/pages/ItemSearchPage.tsx | |
+++ b/src/pages/ItemSearchPage.tsx | |
@@ -78,51 +78,54 @@ export const ItemSearchPage = () => { | |
const handleTextInput = useCallback( | |
(event: ItemSearchFormProps) => { | |
const stringWithoutDashes = event.name.replace(/-/g, ""); | |
- setSearchParams((prev) => { | |
- let updatedSearchParams = prev; | |
- if (/^[0-9]+$/.test(stringWithoutDashes)) { | |
- // set upc | |
- updatedSearchParams.set("upc", event.name); | |
- updatedSearchParams.delete("itemName"); | |
- } else { | |
- // set itemName | |
- updatedSearchParams.set("itemName", event.name); | |
- updatedSearchParams.delete("upc"); | |
- } | |
+ setSearchParams( | |
+ (prev) => { | |
+ let updatedSearchParams = prev; | |
+ if (/^[0-9]+$/.test(stringWithoutDashes)) { | |
+ // set upc | |
+ updatedSearchParams.set("upc", event.name); | |
+ updatedSearchParams.delete("itemName"); | |
+ } else { | |
+ // set itemName | |
+ updatedSearchParams.set("itemName", event.name); | |
+ updatedSearchParams.delete("upc"); | |
+ } | |
- if (event.size) { | |
- updatedSearchParams.set("size", event.size); | |
- } else { | |
- updatedSearchParams.delete("size"); | |
- } | |
+ if (event.size) { | |
+ updatedSearchParams.set("size", event.size); | |
+ } else { | |
+ updatedSearchParams.delete("size"); | |
+ } | |
- if (event.sizeUnit) { | |
- updatedSearchParams.set("sizeUnit", event.sizeUnit); | |
- } | |
+ if (event.sizeUnit) { | |
+ updatedSearchParams.set("sizeUnit", event.sizeUnit); | |
+ } | |
- if (event.department) { | |
- updatedSearchParams.set("department", event.department); | |
- } else { | |
- updatedSearchParams.delete("department"); | |
- } | |
+ if (event.department) { | |
+ updatedSearchParams.set("department", event.department); | |
+ } else { | |
+ updatedSearchParams.delete("department"); | |
+ } | |
- if (event.vendor) { | |
- updatedSearchParams.set("vendor", event.vendor); | |
- } else { | |
- updatedSearchParams.delete("vendor"); | |
- } | |
+ if (event.vendor) { | |
+ updatedSearchParams.set("vendor", event.vendor); | |
+ } else { | |
+ updatedSearchParams.delete("vendor"); | |
+ } | |
- if (event.tags.length) { | |
- event.tags.forEach((tag) => { | |
- updatedSearchParams.append("tags", tag.name); | |
- }); | |
- } else { | |
- updatedSearchParams.delete("tags"); | |
- } | |
+ if (event.tags.length) { | |
+ event.tags.forEach((tag) => { | |
+ updatedSearchParams.append("tags", tag.name); | |
+ }); | |
+ } else { | |
+ updatedSearchParams.delete("tags"); | |
+ } | |
- updatedSearchParams.delete("pageNo"); | |
- return updatedSearchParams.toString(); | |
- }); | |
+ updatedSearchParams.delete("pageNo"); | |
+ return updatedSearchParams.toString(); | |
+ }, | |
+ { replace: true }, | |
+ ); | |
}, | |
[setSearchParams], | |
); | |
diff --git a/src/pages/ItemStateMinReport.tsx b/src/pages/ItemStateMinReport.tsx | |
index 88f0277..bd28440 100644 | |
--- a/src/pages/ItemStateMinReport.tsx | |
+++ b/src/pages/ItemStateMinReport.tsx | |
@@ -13,6 +13,7 @@ import { | |
Tr, | |
useToast, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useEffect, useState } from "react"; | |
import { useLabelPrint } from "../components/LabelPrinter"; | |
import Loading from "../components/Loading"; | |
@@ -141,57 +142,62 @@ const ItemStateMinReport = () => { | |
</Tr> | |
</Thead> | |
<Tbody> | |
- {[...violatingItems.values()].map((item) => { | |
+ {[...violatingItems.values()].map((lineItem) => { | |
return ( | |
- <Tr key={item.item.id}> | |
- <Td>{item.item.id}</Td> | |
- <Td>{item.item.name}</Td> | |
- <Td>{item.item.upc}</Td> | |
+ <Tr key={lineItem.item.id}> | |
+ <Td>{lineItem.item.id}</Td> | |
+ <Td>{lineItem.item.name}</Td> | |
+ <Td>{lineItem.item.upc}</Td> | |
<Td> | |
<NumberInput | |
precision={2} | |
maxW={"100px"} | |
- defaultValue={item.item.sellPrice} | |
+ defaultValue={lineItem.item.sellPrice} | |
min={0} | |
- onBlur={(e) => { | |
+ onBlur={async (e) => { | |
if ( | |
- +e.currentTarget.value === item.item.sellPrice | |
+ +e.currentTarget.value === lineItem.item.sellPrice | |
) { | |
return; | |
} | |
- const oldPrice = item.item.sellPrice; | |
- item.item.sellPrice = +e.currentTarget.value; | |
- api | |
- ?.patchItem(item.item.id, { | |
- itemDetails: item.item, | |
- }) | |
- .then(() => { | |
- printMe( | |
- item.item.name, | |
- item.item.sellPrice, | |
- item.item.id, | |
- item.item.upc, | |
- ); | |
- toast({ | |
- title: "Item updated successfully.", | |
- status: "success", | |
- duration: 1500, | |
- isClosable: true, | |
- }); | |
- }) | |
- .catch(() => { | |
- item.item.sellPrice = oldPrice; | |
+ const oldPrice = lineItem.item.sellPrice; | |
+ lineItem.item.sellPrice = +e.currentTarget.value; | |
+ try { | |
+ await api?.patchItem(lineItem.item.id, { | |
+ itemDetails: lineItem.item, | |
}); | |
+ printMe( | |
+ lineItem.item.name, | |
+ lineItem.item.sellPrice, | |
+ lineItem.item.id, | |
+ lineItem.item.upc, | |
+ ); | |
+ toast({ | |
+ title: "Item updated successfully.", | |
+ status: "success", | |
+ duration: 1500, | |
+ isClosable: true, | |
+ }); | |
+ } catch (err) { | |
+ toast({ | |
+ title: "Error updating item.", | |
+ status: "error", | |
+ duration: 1500, | |
+ isClosable: true, | |
+ }); | |
+ Sentry.captureException(err); | |
+ lineItem.item.sellPrice = oldPrice; | |
+ } | |
}} | |
> | |
<NumberInputField outlineColor={"black"} /> | |
</NumberInput> | |
</Td> | |
<Td> | |
- {optionalStringConcatPrefix("", item.currStateMin)} | |
+ {optionalStringConcatPrefix("", lineItem.currStateMin)} | |
</Td> | |
<Td> | |
- {optionalStringConcatPrefix("", item.nextStateMin)} | |
+ {optionalStringConcatPrefix("", lineItem.nextStateMin)} | |
</Td> | |
</Tr> | |
); | |
diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx | |
index 9efd4b9..39b8cdd 100644 | |
--- a/src/pages/Profile.tsx | |
+++ b/src/pages/Profile.tsx | |
@@ -16,6 +16,7 @@ import { | |
VStack, | |
} from "@chakra-ui/react"; | |
import { yupResolver } from "@hookform/resolvers/yup"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useEffect, useState } from "react"; | |
import { Controller, useForm } from "react-hook-form"; | |
import { useLocation, useNavigate } from "react-router-dom"; | |
@@ -65,6 +66,7 @@ export const Profile: React.FC<ProfileProps> = () => { | |
status: "error", | |
isClosable: true, | |
}); | |
+ Sentry.captureException(e); | |
} | |
} | |
}, | |
@@ -122,13 +124,19 @@ export const Profile: React.FC<ProfileProps> = () => { | |
<FormControl> | |
<FormLabel>First Name</FormLabel> | |
<Skeleton isLoaded={!!authorizedUser}> | |
- <Input {...register("firstName", { required: true })} /> | |
+ <Input | |
+ autoComplete="first-name" | |
+ {...register("firstName", { required: true })} | |
+ /> | |
</Skeleton> | |
</FormControl> | |
<FormControl> | |
<FormLabel>Last Name</FormLabel> | |
<Skeleton isLoaded={!!authorizedUser}> | |
- <Input {...register("lastName", { required: true })} /> | |
+ <Input | |
+ autoComplete="family-name" | |
+ {...register("lastName", { required: true })} | |
+ /> | |
</Skeleton> | |
</FormControl> | |
</HStack> | |
diff --git a/src/pages/PurchaseOrderDetailsEditPage.tsx b/src/pages/PurchaseOrderDetailsEditPage.tsx | |
index a266c52..74ef88b 100644 | |
--- a/src/pages/PurchaseOrderDetailsEditPage.tsx | |
+++ b/src/pages/PurchaseOrderDetailsEditPage.tsx | |
@@ -35,6 +35,7 @@ import { | |
useToast, | |
} from "@chakra-ui/react"; | |
import styled from "@emotion/styled"; | |
+import * as Sentry from "@sentry/react"; | |
import { createColumnHelper } from "@tanstack/react-table"; | |
import { | |
ChakraStylesConfig, | |
@@ -577,7 +578,7 @@ const PurchaseOrderDetailsEditPage = () => { | |
}); | |
} | |
}} | |
- onBlur={(e) => { | |
+ onBlur={async (e) => { | |
if (!e.currentTarget.value) return; | |
const wantedAmount = +e.currentTarget.value; | |
if (previousUnitsEntered === wantedAmount) { | |
@@ -595,8 +596,9 @@ const PurchaseOrderDetailsEditPage = () => { | |
v.year === new Date().getFullYear() && | |
v.month === new Date().getMonth() + 1, | |
); | |
- poApi | |
- ?.modifyPurchaseOrderItem({ | |
+ if (!poApi) return; | |
+ try { | |
+ const res = await poApi.modifyPurchaseOrderItem({ | |
poId: +id, | |
itemDetails: { | |
vendorSku: vendorPricing.sku, | |
@@ -611,37 +613,36 @@ const PurchaseOrderDetailsEditPage = () => { | |
caseCost: currentPricingList?.casePrice ?? 0, | |
unitCost: currentPricingList?.bottlePrice ?? 0, | |
}, | |
- }) | |
- .then((res) => { | |
- const dataUpdated = res.data; | |
- previousUnitsEntered = wantedAmount; | |
- if (!poItemDetails) { | |
- original.poDetailVendorPricings.push( | |
- dataUpdated.itemDetails, | |
- ); | |
- } | |
- toast({ | |
- title: `Item: ${ | |
- item.name | |
- } saved with Units Ordered: ${ | |
- dataUpdated.itemDetails.unitsOrdered ?? 0 | |
- } and Cases Ordered: ${ | |
- dataUpdated.itemDetails.casesOrdered ?? 0 | |
- } for Vendor: ${dataUpdated.itemDetails.vendor}`, | |
- status: "success", | |
- duration: 4000, | |
- isClosable: true, | |
- }); | |
- }) | |
- .catch((e) => { | |
- console.error(e); | |
- toast({ | |
- title: `Item: ${item.name} failed to saved for Vendor: ${vendorPricing.vendor}.`, | |
- status: "error", | |
- duration: 2000, | |
- isClosable: true, | |
- }); | |
}); | |
+ const dataUpdated = res.data; | |
+ previousUnitsEntered = wantedAmount; | |
+ if (!poItemDetails) { | |
+ original.poDetailVendorPricings.push( | |
+ dataUpdated.itemDetails, | |
+ ); | |
+ } | |
+ toast({ | |
+ title: `Item: ${ | |
+ item.name | |
+ } saved with Units Ordered: ${ | |
+ dataUpdated.itemDetails.unitsOrdered ?? 0 | |
+ } and Cases Ordered: ${ | |
+ dataUpdated.itemDetails.casesOrdered ?? 0 | |
+ } for Vendor: ${dataUpdated.itemDetails.vendor}`, | |
+ status: "success", | |
+ duration: 4000, | |
+ isClosable: true, | |
+ }); | |
+ } catch (e) { | |
+ console.error(e); | |
+ toast({ | |
+ title: `Item: ${item.name} failed to saved for Vendor: ${vendorPricing.vendor}.`, | |
+ status: "error", | |
+ duration: 2000, | |
+ isClosable: true, | |
+ }); | |
+ Sentry.captureException(e); | |
+ } | |
}} | |
> | |
<NumberInputField | |
@@ -960,26 +961,25 @@ const PurchaseOrderDetailsEditPage = () => { | |
<PermissionedButton | |
allowOverride | |
requires={`item/${focusedItem?.id}:delete`} | |
- onClick={() => { | |
+ onClick={async () => { | |
if (!focusedItem?.id) return; | |
- api | |
- ?.deleteItem(focusedItem.id) | |
- .then(() => { | |
- toast({ | |
- title: "Item deleted successfully.", | |
- status: "success", | |
- duration: 1500, | |
- isClosable: true, | |
- }); | |
- }) | |
- .catch(() => { | |
- toast({ | |
- title: | |
- "Item deletion failed. Please refresh and try again.", | |
- status: "error", | |
- duration: 1500, | |
- }); | |
+ try { | |
+ await api?.deleteItem(focusedItem.id); | |
+ toast({ | |
+ title: "Item deleted successfully.", | |
+ status: "success", | |
+ duration: 1500, | |
+ isClosable: true, | |
+ }); | |
+ } catch (err) { | |
+ toast({ | |
+ title: | |
+ "Item deletion failed. Please refresh and try again.", | |
+ status: "error", | |
+ duration: 1500, | |
}); | |
+ Sentry.captureException(err); | |
+ } | |
blockingModal.onClose(); | |
}} | |
> | |
diff --git a/src/pages/RegisterOpeningPage.tsx b/src/pages/RegisterOpeningPage.tsx | |
index 7e8856e..878dc49 100644 | |
--- a/src/pages/RegisterOpeningPage.tsx | |
+++ b/src/pages/RegisterOpeningPage.tsx | |
@@ -121,6 +121,7 @@ const RegisterOpeningPage = () => { | |
<FormControl> | |
<FormLabel>Register Number</FormLabel> | |
<Select | |
+ name="register" | |
value={registerValue.registerNumber} | |
onChange={(e) => { | |
const value = e.currentTarget.value; | |
@@ -149,6 +150,7 @@ const RegisterOpeningPage = () => { | |
</> | |
)} | |
<Button | |
+ type="submit" | |
isDisabled={!userEntity} | |
onClick={() => { | |
setIsRegisterOpen((prev) => ({ | |
diff --git a/src/pages/SaleCartPage.tsx b/src/pages/SaleCartPage.tsx | |
index 8ce6861..7ef91e0 100644 | |
--- a/src/pages/SaleCartPage.tsx | |
+++ b/src/pages/SaleCartPage.tsx | |
@@ -86,8 +86,8 @@ const SaleCartPage = () => { | |
<StickyTh width={"4vw"}>#</StickyTh> | |
<StickyTh width={"30vw"}>Name</StickyTh> | |
<StickyTh width={"10vw"}>Price</StickyTh> | |
- <StickyTh width={"7vw"}>Discount</StickyTh> | |
<StickyTh width={"10vw"}>Quantity</StickyTh> | |
+ <StickyTh width={"7vw"}>Discount</StickyTh> | |
<StickyTh width={"10vw"}>Env Fee</StickyTh> | |
<StickyTh width={"10vw"}>Deposit</StickyTh> | |
<StickyTh width={"10vw"}>Total</StickyTh> | |
@@ -99,8 +99,8 @@ const SaleCartPage = () => { | |
<Td>{i + 1}</Td> | |
<Td>{item.name}</Td> | |
<Td>{USDollar.format(item.price)}</Td> | |
- <Td>{USDollar.format(item.discount)}</Td> | |
<Td>{item.quantity}</Td> | |
+ <Td>{USDollar.format(item.discount)}</Td> | |
<Td> | |
{USDollar.format(item.environmentFee * item.quantity)} | |
</Td> | |
@@ -114,13 +114,25 @@ const SaleCartPage = () => { | |
<Tr fontWeight={"bold"}> | |
<Td></Td> | |
<Td>Totals:</Td> | |
- <Td></Td> | |
- <Td></Td> | |
+ <Td> | |
+ {USDollar.format( | |
+ transactionCartData.items.reduce((total, a) => { | |
+ return total + a.price; | |
+ }, 0), | |
+ )} | |
+ </Td> | |
<Td> | |
{transactionCartData.items.reduce((total, a) => { | |
return total + a.quantity; | |
}, 0)} | |
</Td> | |
+ <Td> | |
+ {USDollar.format( | |
+ transactionCartData.items.reduce((total, a) => { | |
+ return total + a.discount; | |
+ }, 0), | |
+ )} | |
+ </Td> | |
<Td> | |
{USDollar.format( | |
transactionCartData.items.reduce((total, item) => { | |
diff --git a/src/pages/SalePage/SalePage.tsx b/src/pages/SalePage/SalePage.tsx | |
index 56eb140..ab3da7a 100644 | |
--- a/src/pages/SalePage/SalePage.tsx | |
+++ b/src/pages/SalePage/SalePage.tsx | |
@@ -6,9 +6,7 @@ import { | |
TableContainer, | |
useDisclosure, | |
} from "@chakra-ui/react"; | |
-import { useDebounce } from "@uidotdev/usehooks"; | |
-import { useEffect, useState } from "react"; | |
-import { v4 as uuidv4 } from "uuid"; | |
+import { useEffect } from "react"; | |
import NavBarWithItemSearch from "../../components/navbar/NavBarWithItemSearch"; | |
import usePoleDisplay from "../../components/PoleDisplay"; | |
import { QtyHotKeys } from "../../components/QtyHotKeys"; | |
@@ -21,23 +19,18 @@ import { Miscellaneous } from "../../components/SalesScreenComponents/SalesButto | |
import { PaymentOptions } from "../../components/SalesScreenComponents/SalesButtonGroups/PaymentOptions/PaymentOptions"; | |
import { QuickPicks } from "../../components/SalesScreenComponents/SalesButtonGroups/QuickPicks/QuickPicks"; | |
import { SalesScreenItemsTable } from "../../components/SalesScreenComponents/SalesScreenItemsTable"; | |
-import { useTransactionApi } from "../../config/ItemsApi"; | |
import { useEntitySelected } from "../../context/EntityProvider"; | |
import { useRegisterProvider } from "../../context/RegisterProvider"; | |
import { useSalesContext } from "../../context/Sales/SalesContext"; | |
import { Navigate } from "react-router-dom"; | |
+import Loading from "../../components/Loading"; | |
import { | |
SalesPaymentProvider, | |
useSalesPaymentContext, | |
} from "../../context/Sales/SalesPaymentContext"; | |
import { useKeypress } from "../../hooks/useKeypress"; | |
-import { TransactionCartItem } from "../../model/data-contracts"; | |
-import { | |
- addSingleItem, | |
- calculateTotalPrice, | |
- convertToTransactionItems, | |
-} from "./SalePageUtils"; | |
+import { convertToTransactionItems } from "./SalePageUtils"; | |
const numOrDashRegex = /^[0-9]|[-]$/; | |
const qRegex = /^[qQ]$/; | |
@@ -81,17 +74,14 @@ export const SalePage = () => { | |
export const SalePageInner = () => { | |
const { | |
items: itemDetails, | |
- setItems: setItemDetails, | |
transactionTotalPrice, | |
retrieveItemsLikeUpc, | |
clearCart, | |
- discount, | |
quantity, | |
setQuantity, | |
- setDiscount, | |
- taxExempt, | |
multipleChoices, | |
setMultipleChoices, | |
+ addSingleItemToCart, | |
} = useSalesContext(); | |
const { | |
lastChangeDue, | |
@@ -101,8 +91,6 @@ export const SalePageInner = () => { | |
creditCardPaid, | |
multiplePaymentsModal, | |
} = useSalesPaymentContext(); | |
- const transactionApi = useTransactionApi(); | |
- const itemDetailsDebounced = useDebounce(itemDetails, 100); | |
const blockingModal = useDisclosure({ | |
onOpen() { | |
stopSound.play(); | |
@@ -117,8 +105,7 @@ export const SalePageInner = () => { | |
setQuantity(1); | |
}, | |
}); | |
- const [entity] = useEntitySelected(); | |
- const [requestId] = useState(uuidv4()); | |
+ const [entity, , isEntityLoading] = useEntitySelected(); | |
const { register } = useRegisterProvider(); | |
const poleDisplay = usePoleDisplay(); | |
@@ -128,7 +115,7 @@ export const SalePageInner = () => { | |
useEffect(() => { | |
// open modal if there are multiple choices | |
if (multipleChoices) { | |
- if (multipleChoices.length > 0) { | |
+ if (multipleChoices.items.length > 0) { | |
multChoiceModalOnOpen(); | |
} else { | |
blockingModalOnOpen(); | |
@@ -255,69 +242,7 @@ export const SalePageInner = () => { | |
poleDisplay("Change Due:", `${USDollar.format(lastChangeDue)}`); | |
}, [lastChangeDue, poleDisplay]); | |
- // Apply discount to all applicable items | |
- useEffect(() => { | |
- if (discount === 0) return; | |
- setItemDetails((prev) => { | |
- const m = new Map(prev); | |
- for (const [key, item] of m) { | |
- const oldPrice = item.item.sellPrice; | |
- const newPrice = Math.max( | |
- item.item.sellPrice * (1 - discount / 100), | |
- item.item.isDiscountAllowed | |
- ? item.item.buyPrice ?? 0 | |
- : item.item.sellPrice, | |
- ); | |
- item.discount = oldPrice - newPrice; | |
- item.totalPrice = calculateTotalPrice(item, taxExempt); | |
- m.set(key, item); | |
- } | |
- return m; | |
- }); | |
- setDiscount(0); | |
- }, [discount, setDiscount, setItemDetails, taxExempt]); | |
- | |
- // Set items in cart when itemDetailsDebounced changes | |
- useEffect(() => { | |
- if (!entity?.id) return; | |
- if (itemDetailsDebounced.size === 0) { | |
- transactionApi | |
- ?.emptyCart({ | |
- entityId: entity.id, | |
- registerNumber: register.registerNumber, | |
- }) | |
- .catch((err) => console.error(err)); | |
- return; | |
- } | |
- | |
- const cartItems = [...itemDetailsDebounced.values()].map((item) => { | |
- const cartItem: TransactionCartItem = { | |
- ...item, | |
- ...item.item, | |
- itemId: item.item.id, | |
- entityId: entity.id, | |
- price: item.item.sellPrice, | |
- bottleFee: item.item.department.bottleDeposit, | |
- environmentFee: item.item.department.environmentFee, | |
- upcCode: item.item.upc, | |
- department: item.item.department.name, | |
- registerId: register.registerNumber, | |
- requestId: requestId, | |
- }; | |
- return cartItem; | |
- }); | |
- transactionApi | |
- ?.setItemsInCart({ | |
- items: cartItems, | |
- }) | |
- .catch((err) => console.error(err)); | |
- }, [ | |
- entity?.id, | |
- register.registerNumber, | |
- itemDetailsDebounced, | |
- requestId, | |
- transactionApi, | |
- ]); | |
+ if (isEntityLoading) return <Loading />; | |
if (!register.isRegisterOpen) { | |
return <Navigate to={"/register/open/"} replace />; | |
@@ -328,7 +253,6 @@ export const SalePageInner = () => { | |
<Stack | |
background={"gray.100"} | |
display={"flex"} | |
- // align="center" | |
spacing="10px" | |
overflow="hidden" | |
width="auto" | |
@@ -340,21 +264,7 @@ export const SalePageInner = () => { | |
<NavBarWithItemSearch | |
onSearchSelected={(itemSelected) => { | |
if (!itemSelected) return; | |
- setItemDetails((prev) => { | |
- const m = new Map(prev); | |
- const item = addSingleItem(itemSelected, m, quantity, taxExempt); | |
- poleDisplay( | |
- `${item.quantity} ${item.item.name.trim().substring(0, 9)} $${( | |
- item.item.sellPrice - item.discount | |
- ).toFixed(2)}`, | |
- `Grand Total $${( | |
- transactionTotalPrice() + item.totalPrice | |
- ).toFixed(2)}`, | |
- ); | |
- m.set(itemSelected.id, item); | |
- setQuantity(1); | |
- return m; | |
- }); | |
+ addSingleItemToCart(itemSelected, quantity); | |
}} | |
/> | |
<Stack direction={"row"}> | |
@@ -412,7 +322,7 @@ export const SalePageInner = () => { | |
<MultipleChoicesModal | |
multipleChoicesModal={multipleChoicesModal} | |
transactionTotalPrice={transactionTotalPrice} | |
- multipleChoices={multipleChoices ?? []} | |
+ multipleChoices={multipleChoices ?? { items: [], quantity: 1 }} | |
/> | |
<ItemNotInSystemAlert blockingModal={blockingModal} /> | |
</> | |
diff --git a/src/pages/SalePage/SalePageUtils.tsx b/src/pages/SalePage/SalePageUtils.tsx | |
index 8ee8a14..bd6c561 100644 | |
--- a/src/pages/SalePage/SalePageUtils.tsx | |
+++ b/src/pages/SalePage/SalePageUtils.tsx | |
@@ -1,69 +1,66 @@ | |
-import _ from "lodash"; | |
-import { Item, TransactionData } from "../../model/data-contracts"; | |
+import { | |
+ Item, | |
+ PromotionAmount, | |
+ TransactionData, | |
+} from "../../model/data-contracts"; | |
-export type ItemWithQty = { | |
+export type LineItem = { | |
item: Item; | |
quantity: number; | |
quantityAsString: string; | |
tax: number; | |
totalPrice: number; | |
discount: number; | |
+ discountOff?: number; | |
+ taxExempt?: boolean; | |
+ bottleDeposit: number; | |
+ environmentFee: number; | |
+ subtotal: number; | |
+ promotionAmounts?: PromotionAmount[]; | |
}; | |
-export function calculateTotalPrice(item: ItemWithQty, taxExempt = false) { | |
- const total = | |
- (item.item.sellPrice - item.discount) * | |
- item.quantity * | |
- ((taxExempt ? 0 : item.tax) + 1) + | |
- (item.item.department.bottleDeposit + item.item.department.environmentFee) * | |
- item.quantity; | |
- return _.round(total, 2); | |
-} | |
- | |
export function addSingleItem( | |
retrievedItem: Item, | |
- m: Map<number, ItemWithQty>, | |
+ m: Map<number, LineItem>, | |
quantity = 1, | |
taxExempt = false, | |
) { | |
- const item = m.get(retrievedItem.id) ?? { | |
+ const lineItem = m.get(retrievedItem.id) ?? { | |
item: retrievedItem, | |
quantity: 0, | |
- tax: retrievedItem.department.tax, | |
+ tax: 0, | |
+ bottleDeposit: 0, | |
+ environmentFee: 0, | |
+ subtotal: 0, | |
totalPrice: 0, | |
discount: 0, | |
quantityAsString: "0", | |
}; | |
- item.quantity += quantity; | |
- item.quantityAsString = `${item.quantity}`; | |
- item.totalPrice = calculateTotalPrice(item, taxExempt); | |
- m.set(retrievedItem.id, item); | |
- return item; | |
+ lineItem.quantity += quantity; | |
+ lineItem.quantityAsString = `${lineItem.quantity}`; | |
+ m.set(retrievedItem.id, lineItem); | |
+ return lineItem; | |
} | |
export const convertToTransactionItems = ( | |
- itemDetails: Map<number, ItemWithQty>, | |
+ itemDetails: Map<number, LineItem>, | |
taxExempt: boolean, | |
) => { | |
const transactionItems: TransactionData[] = []; | |
- for (const [id, item] of itemDetails) { | |
- const tax = +( | |
- item.item.department.tax * | |
- (item.item.sellPrice - item.discount) * | |
- item.quantity | |
- ).toFixed(2); | |
+ for (const [id, lineItem] of itemDetails) { | |
transactionItems.push({ | |
- bottleFee: item.item.department.bottleDeposit * item.quantity, | |
- discount: item.discount * item.quantity, | |
- environmentFee: item.item.department.environmentFee * item.quantity, | |
+ bottleFee: lineItem.bottleDeposit, | |
+ discount: lineItem.discount, | |
+ environmentFee: lineItem.environmentFee, | |
itemId: id, | |
- name: item.item.name, | |
- price: item.item.sellPrice - item.discount, | |
- quantity: item.quantity, | |
- tax: taxExempt ? 0 : tax, | |
- totalPrice: item.totalPrice, | |
- upcCode: item.item.upc, | |
- department: taxExempt ? "NonTaxable" : item.item.department.name, // only used by backend when it is NEW ITEM | |
+ name: lineItem.item.name, | |
+ price: lineItem.item.sellPrice, | |
+ quantity: lineItem.quantity, | |
+ tax: lineItem.tax, | |
+ totalPrice: lineItem.totalPrice, | |
+ upcCode: lineItem.item.upc, | |
+ department: taxExempt ? "NonTaxable" : lineItem.item.department.name, // only used by backend when it is NEW ITEM | |
+ promotionItems: lineItem.promotionAmounts, | |
}); | |
} | |
return transactionItems; | |
diff --git a/src/pages/Settings/AuditLogs/AuditLogsPanel.tsx b/src/pages/Settings/AuditLogs/AuditLogsPanel.tsx | |
index b07d831..c2ee8fc 100644 | |
--- a/src/pages/Settings/AuditLogs/AuditLogsPanel.tsx | |
+++ b/src/pages/Settings/AuditLogs/AuditLogsPanel.tsx | |
@@ -7,12 +7,16 @@ import { | |
Tooltip, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { CellContext, createColumnHelper } from "@tanstack/react-table"; | |
import moment from "moment"; | |
import { useEffect, useMemo, useState } from "react"; | |
import { FaEye } from "react-icons/fa"; | |
import { Link } from "react-router-dom"; | |
-import { EventTypeTag } from "../../../components/Events/EventTypeTag"; | |
+import { | |
+ EventType, | |
+ EventTypeTag, | |
+} from "../../../components/Events/EventTypeTag"; | |
import { SettingsBlock } from "../../../components/SettingsComponents/SettingsBlock"; | |
import { TableControls } from "../../../components/Table/TableControls"; | |
import { TransformityDataTable } from "../../../components/Table/TransformityDataTable"; | |
@@ -67,7 +71,7 @@ const PayloadCell = ({ getValue, row }: CellContext<EventDTO, any>) => { | |
<LinkToResource | |
id={payload.invoice?.id} | |
link={"/invoices"} | |
- label={"Invoice: " + payload.invoice.invoiceId} | |
+ label={"Invoice: " + payload.invoice?.invoiceId} | |
/> | |
); | |
case "TRANSACTION": | |
@@ -75,7 +79,7 @@ const PayloadCell = ({ getValue, row }: CellContext<EventDTO, any>) => { | |
<LinkToResource | |
id={payload.transaction?.id} | |
link={"/transaction"} | |
- label={"Transaction: " + payload.id} | |
+ label={"Transaction: " + payload.transaction?.id} | |
/> | |
); | |
default: | |
@@ -121,31 +125,28 @@ export const AuditLogsPanel: React.FC<AuditLogsPanelProps> = () => { | |
setIsLoading(true); | |
const getEvents = async () => { | |
- const events = await entityApi.getEventsForEntity(entity.id, { | |
- // TODO: fix pagesination | |
- page: pageState.number - 1, | |
- size: pageState.size, | |
- sort: sortStateAsString, | |
- eventType: selectedType, | |
- startDate: selectedDate[0].toISOString(), | |
- endDate: selectedDate[1].toISOString(), | |
- }); | |
- return events.data; | |
- }; | |
- | |
- getEvents() | |
- .then((events) => { | |
+ try { | |
+ const events = await entityApi.getEventsForEntity(entity.id, { | |
+ // TODO: fix pagesination | |
+ page: pageState.number - 1, | |
+ size: pageState.size, | |
+ sort: sortStateAsString, | |
+ eventType: selectedType, | |
+ startDate: selectedDate[0].toISOString(), | |
+ endDate: selectedDate[1].toISOString(), | |
+ }); | |
setPageState({ | |
- ...events.page, | |
- number: events.page.number + 1, | |
+ ...events.data.page, | |
+ number: events.data.page.number + 1, | |
}); | |
- setDisplayData(events.items); | |
+ setDisplayData(events.data.items); | |
setIsLoading(false); | |
- }) | |
- .catch((err) => { | |
+ } catch (err) { | |
console.log(err); | |
+ Sentry.captureException(err); | |
setIsLoading(false); | |
- }); | |
+ } | |
+ }; | |
}, [ | |
entity, | |
entityApi, | |
@@ -222,20 +223,10 @@ export const AuditLogsPanel: React.FC<AuditLogsPanelProps> = () => { | |
<TableControls | |
controls={[ | |
{ | |
- options: [ | |
- { | |
- label: "Item Created", | |
- value: "ITEM_CREATED", | |
- }, | |
- { | |
- label: "Item Updated", | |
- value: "ITEM_UPDATED", | |
- }, | |
- { | |
- label: "Item Deleted", | |
- value: "ITEM_DELETED", | |
- }, | |
- ], | |
+ options: Object.entries(EventType).map(([name, value]) => ({ | |
+ label: value.name, | |
+ value: name, | |
+ })), | |
onChange: (newVal) => { | |
setSelectedType(newVal?.value); | |
}, | |
diff --git a/src/pages/Settings/Devices/DeviceManagementPanel.tsx b/src/pages/Settings/Devices/DeviceManagementPanel.tsx | |
index 57cf84e..8d0c5d4 100644 | |
--- a/src/pages/Settings/Devices/DeviceManagementPanel.tsx | |
+++ b/src/pages/Settings/Devices/DeviceManagementPanel.tsx | |
@@ -21,10 +21,10 @@ export const DeviceManagementPanel: React.FC< | |
</Button> | |
</VStack> | |
<VStack w="100%" justifyContent="space-between" alignItems="flex-start"> | |
- <Heading size="md" onClick={() => requestCashDrawer()}> | |
- Cash Drawer | |
- </Heading> | |
- <Button variant="outline">Set up Cash Drawer</Button> | |
+ <Heading size="md">Cash Drawer</Heading> | |
+ <Button variant="outline" onClick={() => requestCashDrawer()}> | |
+ Set up Cash Drawer | |
+ </Button> | |
</VStack> | |
</VStack> | |
</SettingsBlock> | |
diff --git a/src/pages/Settings/Roles/RoleManagementModal.tsx b/src/pages/Settings/Roles/RoleManagementModal.tsx | |
index e5f7d6b..184d972 100644 | |
--- a/src/pages/Settings/Roles/RoleManagementModal.tsx | |
+++ b/src/pages/Settings/Roles/RoleManagementModal.tsx | |
@@ -1,4 +1,5 @@ | |
import { Button, ButtonGroup, useToast, VStack } from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useState } from "react"; | |
import { FormProvider, useForm } from "react-hook-form"; | |
import { BasicModal } from "../../../components/common/BasicModal"; | |
@@ -35,34 +36,33 @@ export const RoleManagementModal: React.FC<RoleManagementModalProps> = ({ | |
} | |
setIsLoading(true); | |
- entityApi | |
- .patchPermissionsForEntity(entity.id, [ | |
+ try { | |
+ const resp = await entityApi.patchPermissionsForEntity(entity.id, [ | |
{ role: role.role, permissions: methods.getValues().value }, | |
- ]) | |
- .then((resp) => { | |
- setRole(undefined); | |
- toast({ | |
- title: "Success", | |
- description: "Successfully updated role permissions.", | |
- status: "success", | |
- duration: 5000, | |
- isClosable: true, | |
- }); | |
- setRoleData(resp.data); | |
- window.location.reload(); | |
- }) | |
- .catch(() => { | |
- toast({ | |
- title: "Error", | |
- description: "Failed to update role permissions.", | |
- status: "error", | |
- duration: 5000, | |
- isClosable: true, | |
- }); | |
- }) | |
- .finally(() => { | |
- setIsLoading(false); | |
+ ]); | |
+ | |
+ setRole(undefined); | |
+ toast({ | |
+ title: "Success", | |
+ description: "Successfully updated role permissions.", | |
+ status: "success", | |
+ duration: 5000, | |
+ isClosable: true, | |
+ }); | |
+ setRoleData(resp.data); | |
+ window.location.reload(); | |
+ } catch (e) { | |
+ toast({ | |
+ title: "Error", | |
+ description: "Failed to update role permissions.", | |
+ status: "error", | |
+ duration: 5000, | |
+ isClosable: true, | |
}); | |
+ Sentry.captureException(e); | |
+ } finally { | |
+ setIsLoading(false); | |
+ } | |
}, [entity, entityApi, methods, role, setRole, setRoleData, toast]); | |
return ( | |
diff --git a/src/pages/Settings/SettingsPage.tsx b/src/pages/Settings/SettingsPage.tsx | |
index b4e6848..6406eff 100644 | |
--- a/src/pages/Settings/SettingsPage.tsx | |
+++ b/src/pages/Settings/SettingsPage.tsx | |
@@ -14,7 +14,7 @@ import { UsersSettingsPanel } from "./Users/UsersSettingsPanel"; | |
export type SettingsPageProps = {}; | |
-export const SettingsPage: React.FC<SettingsPageProps> = ({}) => { | |
+export const SettingsPage: React.FC<SettingsPageProps> = () => { | |
const { hasPermission } = usePermissions(); | |
const [entity] = useEntitySelected(); | |
const { pathname } = useLocation(); | |
diff --git a/src/pages/Settings/Tags/TagsSettingsPanel.tsx b/src/pages/Settings/Tags/TagsSettingsPanel.tsx | |
index db0fd76..35920d9 100644 | |
--- a/src/pages/Settings/Tags/TagsSettingsPanel.tsx | |
+++ b/src/pages/Settings/Tags/TagsSettingsPanel.tsx | |
@@ -13,6 +13,7 @@ import { | |
useDisclosure, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useEffect, useState } from "react"; | |
import { SearchBar } from "../../../components/common/SearchBar/SearchBar"; | |
import { SettingsBlock } from "../../../components/SettingsComponents/SettingsBlock"; | |
@@ -77,15 +78,16 @@ export const TagsSettingsPanel: React.FC<TagsSettingsPanelProps> = ({}) => { | |
useEffect(() => { | |
if (entityApi && entity?.id && !tags && !isError) { | |
setIsLoading(true); | |
- entityApi | |
- .getTags(entity.id) | |
- .then((res) => { | |
+ const getTags = async () => { | |
+ try { | |
+ const res = await entityApi.getTags(entity.id); | |
setTags(res.data.tags); | |
- }) | |
- .catch((e) => { | |
+ } catch (e) { | |
setIsError(true); | |
console.log("Error getting tags", e); | |
- }); | |
+ Sentry.captureException(e); | |
+ } | |
+ }; | |
} | |
if (tags || isError) setIsLoading(false); | |
}, [entityApi, entity?.id, tags, isError]); | |
diff --git a/src/pages/Settings/Users/AddUserModal.tsx b/src/pages/Settings/Users/AddUserModal.tsx | |
index 13682d8..b6be698 100644 | |
--- a/src/pages/Settings/Users/AddUserModal.tsx | |
+++ b/src/pages/Settings/Users/AddUserModal.tsx | |
@@ -9,6 +9,7 @@ import { | |
useToast, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useCallback, useState } from "react"; | |
import { useForm } from "react-hook-form"; | |
import { BasicModal } from "../../../components/common/BasicModal"; | |
@@ -34,35 +35,34 @@ export const AddUserModal: React.FC<AddUserModalProps> = ({ | |
const toast = useToast(); | |
const onSubmit = useCallback( | |
- (data: AddUserFormType) => { | |
+ async (data: AddUserFormType) => { | |
if (!entityApi || !entity) { | |
return; | |
} | |
setIsLoading(true); | |
- entityApi | |
- .addUserToEntity(entity.id, data) | |
- .then(() => { | |
- disclosure.onClose(); | |
- setIsLoading(false); | |
- toast({ | |
- title: "Success", | |
- description: "User added successfully", | |
- status: "success", | |
- duration: 3000, | |
- isClosable: true, | |
- }); | |
- }) | |
- .catch(() => { | |
- setIsLoading(false); | |
- toast({ | |
- title: "Error", | |
- description: "There was an error adding the user", | |
- status: "error", | |
- duration: 3000, | |
- isClosable: true, | |
- }); | |
+ try { | |
+ await entityApi.addUserToEntity(entity.id, data); | |
+ disclosure.onClose(); | |
+ setIsLoading(false); | |
+ toast({ | |
+ title: "Success", | |
+ description: "User added successfully", | |
+ status: "success", | |
+ duration: 3000, | |
+ isClosable: true, | |
}); | |
+ } catch (e) { | |
+ setIsLoading(false); | |
+ toast({ | |
+ title: "Error", | |
+ description: "There was an error adding the user", | |
+ status: "error", | |
+ duration: 3000, | |
+ isClosable: true, | |
+ }); | |
+ Sentry.captureException(e); | |
+ } | |
}, | |
[disclosure, entity, entityApi, toast], | |
); | |
diff --git a/src/pages/Settings/Users/UsersSettingsPanel.tsx b/src/pages/Settings/Users/UsersSettingsPanel.tsx | |
index b30f4c2..665eb8a 100644 | |
--- a/src/pages/Settings/Users/UsersSettingsPanel.tsx | |
+++ b/src/pages/Settings/Users/UsersSettingsPanel.tsx | |
@@ -42,7 +42,11 @@ export const UsersSettingsPanel: React.FC<UsersSettingsPanelProps> = () => { | |
return; | |
} | |
- entityApi.getUsersByEntity(entity.id).then((rep) => setUsers(rep.data)); | |
+ entityApi | |
+ .getUsersByEntity(entity.id, { | |
+ roles: [Role.OWNER, Role.ADMIN, Role.CASHIER, Role.MANAGER], | |
+ }) | |
+ .then((rep) => setUsers(rep.data)); | |
}, [setUsers, entityApi, entity]); | |
const columns = useMemo( | |
diff --git a/src/pages/SignUp.tsx b/src/pages/SignUp.tsx | |
index cf2f140..a635cf8 100644 | |
--- a/src/pages/SignUp.tsx | |
+++ b/src/pages/SignUp.tsx | |
@@ -12,6 +12,7 @@ import { | |
Stack, | |
Text, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { useState } from "react"; | |
import { useNavigate } from "react-router-dom"; | |
import { OAuthButtonGroup } from "../components/Auth/OAuthButtonGroup"; | |
@@ -32,6 +33,7 @@ export const SignUp = () => { | |
await createUser!(email, password); | |
navigate("/items/search"); | |
} catch (e: any) { | |
+ Sentry.captureException(e); | |
setError(e.message); | |
console.log(e.message); | |
} | |
@@ -43,6 +45,7 @@ export const SignUp = () => { | |
await googleSignIn!(); | |
navigate("/items/search"); | |
} catch (e: any) { | |
+ Sentry.captureException(e); | |
setError(e.message); | |
console.log(e.message); | |
} | |
diff --git a/src/pages/TransactionHistoryByClosing.tsx b/src/pages/TransactionHistoryByClosing.tsx | |
index dc09f64..b934dc1 100644 | |
--- a/src/pages/TransactionHistoryByClosing.tsx | |
+++ b/src/pages/TransactionHistoryByClosing.tsx | |
@@ -11,6 +11,7 @@ import moment from "moment"; | |
import { useEffect, useState } from "react"; | |
import { FaPrint } from "react-icons/fa"; | |
import { Link } from "react-router-dom"; | |
+import { DateTimeParam, useQueryParam } from "use-query-params"; | |
import DepartmentSummary from "../components/DepartmentSummary"; | |
import Loading from "../components/Loading"; | |
import { NavBarPlain } from "../components/navbar/NavBarPlain"; | |
@@ -25,24 +26,33 @@ import { TransactionReportByDepartmentOutput } from "../model/data-contracts"; | |
export const TransactionHistoryByClosing = () => { | |
const transactionsApi = useTransactionsApi(); | |
const [report, setReport] = useState<TransactionReportByDepartmentOutput>(); | |
+ const [entity] = useEntitySelected(); | |
+ | |
+ const [startDate, setStartDate] = useQueryParam("startDate", DateTimeParam); | |
+ const [endDate, setEndDate] = useQueryParam("endDate", DateTimeParam); | |
+ | |
const [selectedDates, setSelectedDates] = useState<Date[]>([ | |
- moment().subtract(1, "month").startOf("day").toDate(), | |
- moment().endOf("day").toDate(), | |
+ startDate ?? moment().subtract(1, "month").startOf("day").toDate(), | |
+ endDate ?? moment().endOf("day").toDate(), | |
]); | |
- const [entity] = useEntitySelected(); | |
- const startDate = moment(selectedDates[0]).startOf("day").toISOString(); | |
- const endDate = moment(selectedDates[1] ?? new Date()) | |
- .endOf("day") | |
- .toISOString(); | |
+ const tempStartDate = selectedDates[0]; | |
+ useEffect(() => { | |
+ setStartDate(moment(tempStartDate).startOf("day").toDate()); | |
+ }, [tempStartDate, setStartDate]); | |
+ | |
+ const tempEndDate = selectedDates[1] ?? new Date(); | |
+ useEffect(() => { | |
+ setEndDate(moment(tempEndDate).endOf("day").toDate()); | |
+ }); | |
useEffect(() => { | |
- if (selectedDates.length < 2) return; | |
+ if (!startDate || !endDate) return; | |
if (!entity?.id) return; | |
transactionsApi | |
?.transactionReportByDepartment({ | |
- startDate: startDate, | |
- endDate: endDate, | |
+ startDate: startDate.toISOString(), | |
+ endDate: endDate.toISOString(), | |
// Means get anything closed | |
closingsId: 1, | |
entityId: entity.id, | |
@@ -51,13 +61,15 @@ export const TransactionHistoryByClosing = () => { | |
.then((res) => { | |
setReport(res.data); | |
}); | |
- }, [endDate, entity?.id, selectedDates, startDate, transactionsApi]); | |
+ }, [endDate, entity?.id, startDate, transactionsApi]); | |
if (!report) return <Loading />; | |
+ if (!startDate || !endDate) return <></>; | |
+ | |
const printParams = new URLSearchParams(); | |
- printParams.append("startDate", startDate); | |
- printParams.append("endDate", endDate); | |
+ printParams.append("startDate", startDate.toISOString()); | |
+ printParams.append("endDate", endDate.toISOString()); | |
return ( | |
<> | |
diff --git a/src/pages/TransactionHistoryByDatePage.tsx b/src/pages/TransactionHistoryByDatePage.tsx | |
index 8ac1708..2997bf6 100644 | |
--- a/src/pages/TransactionHistoryByDatePage.tsx | |
+++ b/src/pages/TransactionHistoryByDatePage.tsx | |
@@ -1,6 +1,7 @@ | |
import { Card, Flex, Heading, Spacer, Stack } from "@chakra-ui/react"; | |
import moment from "moment"; | |
import { useEffect, useState } from "react"; | |
+import { DateTimeParam, useQueryParam } from "use-query-params"; | |
import DepartmentSummary from "../components/DepartmentSummary"; | |
import { NavBarPlain } from "../components/navbar/NavBarPlain"; | |
import PaymentSummary from "../components/PaymentSummary"; | |
@@ -14,37 +15,48 @@ import { TransactionReportByDateOutput } from "../model/data-contracts"; | |
export const TransactionHistoryByClosing = () => { | |
const transactionsApi = useTransactionsApi(); | |
const [report, setReport] = useState<TransactionReportByDateOutput>(); | |
+ | |
+ const [startDate, setStartDate] = useQueryParam("startDate", DateTimeParam); | |
+ const [endDate, setEndDate] = useQueryParam("endDate", DateTimeParam); | |
+ | |
const [selectedDates, setSelectedDates] = useState<Date[]>([ | |
- moment().startOf("day").toDate(), | |
- moment().endOf("day").toDate(), | |
+ startDate ?? moment().startOf("day").toDate(), | |
+ endDate ?? moment().endOf("day").toDate(), | |
]); | |
- const [entity] = useEntitySelected(); | |
+ const tempStartDate = selectedDates[0]; | |
+ useEffect(() => { | |
+ setStartDate(moment(tempStartDate).startOf("day").toDate()); | |
+ }, [tempStartDate, setStartDate]); | |
- const startDate = selectedDates[0].toISOString(); | |
- const endDate = moment(selectedDates[1] ?? new Date()) | |
- .toDate() | |
- .toISOString(); | |
+ const tempEndDate = selectedDates[1] ?? new Date(); | |
+ useEffect(() => { | |
+ setEndDate(moment(tempEndDate).endOf("day").toDate()); | |
+ }); | |
+ | |
+ const [entity] = useEntitySelected(); | |
useEffect(() => { | |
- if (selectedDates.length < 2) return; | |
if (!entity?.id) return; | |
+ if (!startDate || !endDate) return; | |
+ | |
transactionsApi | |
?.transactionReportByDate({ | |
- startDate: startDate, | |
- endDate: endDate, | |
+ startDate: startDate.toISOString(), | |
+ endDate: endDate.toISOString(), | |
entityId: entity.id, | |
zoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, | |
}) | |
.then((res) => { | |
setReport(res.data); | |
}); | |
- }, [endDate, entity?.id, selectedDates, startDate, transactionsApi]); | |
+ }, [endDate, entity?.id, startDate, transactionsApi]); | |
if (!report) return <></>; | |
+ if (!startDate || !endDate) return <></>; | |
const printParams = new URLSearchParams(); | |
- printParams.append("startDate", startDate); | |
- printParams.append("endDate", endDate); | |
+ printParams.append("startDate", startDate.toISOString()); | |
+ printParams.append("endDate", endDate.toISOString()); | |
return ( | |
<> | |
@@ -84,7 +96,8 @@ export const TransactionHistoryByClosing = () => { | |
summary.transactionItems.reduce( | |
(partialSum, item) => | |
partialSum + | |
- item.price * item.quantity + | |
+ item.price * item.quantity - | |
+ item.discount + | |
item.bottleFee + | |
item.environmentFee, | |
0, | |
diff --git a/src/pages/TransactionPage.tsx b/src/pages/TransactionPage.tsx | |
index 3ac5aee..77e2c3a 100644 | |
--- a/src/pages/TransactionPage.tsx | |
+++ b/src/pages/TransactionPage.tsx | |
@@ -28,6 +28,7 @@ import VoidSale from "../components/VoidSale"; | |
import { useTransactionApi } from "../config/ItemsApi"; | |
import { SalesPaymentProvider } from "../context/Sales/SalesPaymentContext"; | |
import { TransactionOutput } from "../model/data-contracts"; | |
+import { calculateSubtotal } from "../utils/numberUtils"; | |
import { USDollar } from "./SalePage/SalePage"; | |
function itemComp(itemMap: TransactionOutput["transactionItems"]) { | |
@@ -42,11 +43,11 @@ function itemComp(itemMap: TransactionOutput["transactionItems"]) { | |
<Td>{transaction.name}</Td> | |
<Td>{transaction.upcCode}</Td> | |
<Td>{USDollar.format(transaction.price)}</Td> | |
- <Td>{USDollar.format(transaction.discount)}</Td> | |
<Td>{transaction.quantity}</Td> | |
+ <Td>{USDollar.format(transaction.discount)}</Td> | |
<Td>{USDollar.format(transaction.bottleFee)}</Td> | |
<Td>{USDollar.format(transaction.environmentFee)}</Td> | |
- <Td>{USDollar.format(transaction.price * transaction.quantity)}</Td> | |
+ <Td>{USDollar.format(calculateSubtotal(transaction))}</Td> | |
<Td>{USDollar.format(transaction.totalPrice)}</Td> | |
</Tr>, | |
); | |
@@ -139,8 +140,8 @@ const TransactionPage = () => { | |
<Th width={"10vw"}>Name</Th> | |
<Th width={"10vw"}>UPC</Th> | |
<Th width={"10vw"}>Price</Th> | |
- <Th width={"10vw"}>Discount</Th> | |
<Th width={"10vw"}>Quantity</Th> | |
+ <Th width={"10vw"}>Discount</Th> | |
<Th width={"10vw"}>Bottle Dep</Th> | |
<Th width={"10vw"}>Env Fee</Th> | |
<Th width={"10vw"}>Sub-total</Th> | |
diff --git a/src/pages/TransactionPagePrint.tsx b/src/pages/TransactionPagePrint.tsx | |
index 458b48e..3552e96 100644 | |
--- a/src/pages/TransactionPagePrint.tsx | |
+++ b/src/pages/TransactionPagePrint.tsx | |
@@ -2,13 +2,14 @@ import { Fragment, useEffect, useState } from "react"; | |
import { useParams } from "react-router-dom"; | |
import { useTransactionApi } from "../config/ItemsApi"; | |
import { useEntitySelected } from "../context/EntityProvider"; | |
+import { useRegisterProvider } from "../context/RegisterProvider"; | |
import { TransactionOutput } from "../model/data-contracts"; | |
const TransactionPagePrint = () => { | |
const transaction = useTransactionApi(); | |
- const [itemDetails, setItemDetails] = useState<TransactionOutput>(); | |
const { txId } = useParams(); | |
const [entity] = useEntitySelected(); | |
+ const { register } = useRegisterProvider(); | |
const [transactionOutput, setTransactionOutput] = | |
useState<TransactionOutput>(); | |
@@ -21,10 +22,7 @@ const TransactionPagePrint = () => { | |
useEffect(() => { | |
if (!txId || !entity?.id || !transactionOutput) return; | |
- setItemDetails(transactionOutput); | |
- setTimeout(() => { | |
- window.print(); | |
- }, 500); | |
+ window.print(); | |
}, [txId, transactionOutput, entity?.id]); | |
useEffect(() => { | |
@@ -35,7 +33,7 @@ const TransactionPagePrint = () => { | |
}; | |
}, [entity?.id]); | |
- if (!itemDetails) return <></>; | |
+ if (!transactionOutput) return <></>; | |
return ( | |
<> | |
@@ -51,17 +49,20 @@ const TransactionPagePrint = () => { | |
)} | |
<br /> | |
<div style={{ display: "flex", justifyContent: "space-between" }}> | |
- <p>Trans: {txId}</p> | |
+ <p>Tran: {txId} </p> | |
<p> | |
- {new Date(Date.parse(itemDetails.createdDate)).toLocaleString([], { | |
- year: "numeric", | |
- month: "numeric", | |
- day: "numeric", | |
- hour: "2-digit", | |
- minute: "2-digit", | |
- })} | |
+ {new Date(Date.parse(transactionOutput.createdDate)).toLocaleString( | |
+ [], | |
+ { | |
+ year: "numeric", | |
+ month: "numeric", | |
+ day: "numeric", | |
+ hour: "2-digit", | |
+ minute: "2-digit", | |
+ }, | |
+ )} | |
</p> | |
- <p>Register: 1</p> | |
+ <p>Reg: {register.registerNumber}</p> | |
</div> | |
<table style={{ width: "100%" }}> | |
<thead | |
@@ -79,7 +80,7 @@ const TransactionPagePrint = () => { | |
</tr> | |
</thead> | |
<tbody> | |
- {itemDetails.transactionItems.map((itemDetail, index) => { | |
+ {transactionOutput.transactionItems.map((itemDetail, index) => { | |
return ( | |
<Fragment key={index}> | |
<tr> | |
@@ -107,12 +108,12 @@ const TransactionPagePrint = () => { | |
<td></td> | |
<td></td> | |
<td> | |
- {itemDetails.transactionItems | |
+ {transactionOutput.transactionItems | |
.reduce((prev, a) => prev + a.tax, 0) | |
.toFixed(2)} | |
</td> | |
<td> | |
- {itemDetails.transactionItems | |
+ {transactionOutput.transactionItems | |
.reduce((prev, a) => prev + a.totalPrice, 0) | |
.toFixed(2)} | |
</td> | |
@@ -121,7 +122,7 @@ const TransactionPagePrint = () => { | |
<td colSpan={3}>Discount Total</td> | |
<td></td> | |
<td> | |
- {itemDetails.transactionItems | |
+ {transactionOutput.transactionItems | |
.reduce((prev, a) => prev + a.discount, 0) | |
.toFixed(2)} | |
</td> | |
@@ -130,7 +131,7 @@ const TransactionPagePrint = () => { | |
</tbody> | |
</table> | |
<div> | |
- <p>Payment Form: {itemDetails.paymentForm}</p> | |
+ <p>Payment Form: {transactionOutput.paymentForm}</p> | |
</div> | |
{transactionOutput?.payinId && ( | |
<> | |
@@ -146,7 +147,7 @@ const TransactionPagePrint = () => { | |
</> | |
)} | |
<div> | |
- <p>Cash Paid: {itemDetails.amountReceived.toFixed(2)}</p> | |
+ <p>Cash Paid: {transactionOutput.amountReceived.toFixed(2)}</p> | |
</div> | |
<h1 style={{ textAlign: "center" }}>Thank you for shopping with us!</h1> | |
<br /> | |
diff --git a/src/pages/TransactionsClosingPage.tsx b/src/pages/TransactionsClosingPage.tsx | |
index ba5ac77..24c7b2b 100644 | |
--- a/src/pages/TransactionsClosingPage.tsx | |
+++ b/src/pages/TransactionsClosingPage.tsx | |
@@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router-dom"; | |
import { useTransactionsApi } from "../config/ItemsApi"; | |
import { useEntitySelected } from "../context/EntityProvider"; | |
import { TransactionsClosingOutput } from "../model/data-contracts"; | |
+import { calculateSubtotal } from "../utils/numberUtils"; | |
import { USDollar } from "./SalePage/SalePage"; | |
const TransactionsClosingPage = () => { | |
@@ -42,7 +43,7 @@ const TransactionsClosingPage = () => { | |
fontSize: "11px", | |
}} | |
> | |
- <h1 style={{ textAlign: "center" }}> | |
+ <h1 data-testid="closing-title" style={{ textAlign: "center" }}> | |
Closing #{data.id}: Date{" "} | |
{`${new Date(Date.parse(data.closeDateTime)).toLocaleString()}`} | |
</h1> | |
@@ -71,17 +72,23 @@ const TransactionsClosingPage = () => { | |
.sort( | |
(a, b) => Date.parse(a.createdDate) - Date.parse(b.createdDate), | |
) | |
- .map((transaction) => { | |
+ .map((transaction, i) => { | |
return ( | |
<tr> | |
- <td>{transaction.id}</td> | |
- <td>{transaction.registerNumber}</td> | |
- <td> | |
+ <td data-testid={`transaction-id-${transaction.id}`}> | |
+ {transaction.id} | |
+ </td> | |
+ <td | |
+ data-testid={`transaction-register-number-${transaction.id}`} | |
+ > | |
+ {transaction.registerNumber} | |
+ </td> | |
+ <td data-testid={`transaction-date-${transaction.id}`}> | |
{new Date( | |
Date.parse(transaction.createdDate), | |
).toLocaleString()} | |
</td> | |
- <td> | |
+ <td data-testid={`transaction-tax-${transaction.id}`}> | |
{USDollar.format( | |
transaction.transactionItems.reduce( | |
(tax, transactionItem) => transactionItem.tax + tax, | |
@@ -89,12 +96,11 @@ const TransactionsClosingPage = () => { | |
), | |
)} | |
</td> | |
- <td> | |
+ <td data-testid={`transaction-subtotal-${transaction.id}`}> | |
{USDollar.format( | |
transaction.transactionItems.reduce( | |
(accumulator, transactionItem) => | |
- accumulator + | |
- transactionItem.price * transactionItem.quantity, | |
+ accumulator + calculateSubtotal(transactionItem), | |
0, | |
), | |
)} | |
@@ -154,7 +160,7 @@ const TransactionsClosingPage = () => { | |
a + | |
transaction.transactionItems.reduce( | |
(b, transactionItem) => | |
- b + transactionItem.price * transactionItem.quantity, | |
+ b + calculateSubtotal(transactionItem), | |
0, | |
), | |
0, | |
@@ -295,7 +301,7 @@ const TransactionsClosingPage = () => { | |
transaction.transactionItems.reduce( | |
(b, transactionItem) => | |
b + | |
- transactionItem.price * transactionItem.quantity + | |
+ calculateSubtotal(transactionItem) + | |
transactionItem.environmentFee + | |
transactionItem.bottleFee, | |
0, | |
diff --git a/src/pages/TransactionsPage.tsx b/src/pages/TransactionsPage.tsx | |
index fb261f4..dd89d46 100644 | |
--- a/src/pages/TransactionsPage.tsx | |
+++ b/src/pages/TransactionsPage.tsx | |
@@ -49,7 +49,7 @@ const TransactionsPage = () => { | |
.then((res) => setReport(res.data)); | |
}, [entity?.id, selectedRegister, transactions]); | |
- if (!isRegisterOpen.isRegisterOpen) return <Navigate to={"/sale"} />; | |
+ if (!isRegisterOpen.isRegisterOpen) return <Navigate to={"/sale/"} />; | |
if (!report) return <Loading />; | |
return ( | |
@@ -61,13 +61,12 @@ const TransactionsPage = () => { | |
align="center" | |
spacing="10px" | |
width="auto" | |
- maxWidth="100%" | |
height={"100%"} | |
overflowY={"auto"} | |
> | |
<NavBarPlain /> | |
<Flex gap="15px" direction={"column"} width="95vw" height="auto"> | |
- <Card> | |
+ <Card data-testid="transactions-report-table"> | |
<CardHeader> | |
<Heading size="md"> | |
<Flex> | |
diff --git a/src/pages/invoices/InvoicePage/InvoiceEditStep/InvoiceEditPageTable.tsx b/src/pages/invoices/InvoicePage/InvoiceEditStep/InvoiceEditPageTable.tsx | |
index 671d682..a10c8c4 100644 | |
--- a/src/pages/invoices/InvoicePage/InvoiceEditStep/InvoiceEditPageTable.tsx | |
+++ b/src/pages/invoices/InvoicePage/InvoiceEditStep/InvoiceEditPageTable.tsx | |
@@ -6,6 +6,7 @@ import { | |
Heading, | |
HStack, | |
IconButton, | |
+ Link, | |
Stat, | |
Text, | |
Tooltip, | |
@@ -14,14 +15,22 @@ import { | |
} from "@chakra-ui/react"; | |
import { createColumnHelper } from "@tanstack/react-table"; | |
import _ from "lodash"; | |
-import { forwardRef, useCallback, useEffect, useMemo, useRef } from "react"; | |
+import { | |
+ forwardRef, | |
+ useCallback, | |
+ useEffect, | |
+ useMemo, | |
+ useRef, | |
+ useState, | |
+} from "react"; | |
import { IoMdSwap } from "react-icons/io"; | |
import { EditableInput } from "../../../../components/common/EditableInput/EditableInput2"; | |
import ItemSearchBox from "../../../../components/ItemSearchComponents/ItemSearchBox"; | |
+import { QuickItemEditModal } from "../../../../components/SalesScreenComponents/Modals/QuickItemEditModal"; | |
import { CurrencyRow } from "../../../../components/Table/CurrencyRow"; | |
import { TransformityDataTable } from "../../../../components/Table/TransformityDataTable"; | |
import { useInvoiceContext } from "../../../../context/InvoiceContext"; | |
-import { InvoiceItem } from "../../../../model/data-contracts"; | |
+import { InvoiceItem, ItemDetails } from "../../../../model/data-contracts"; | |
import { removeElementFromArray } from "../../../../utils/arrayUtils"; | |
export const getNetUnitCost = (item: InvoiceItem | undefined) => { | |
@@ -54,6 +63,16 @@ export const isInvoiceItemReturn = (item: InvoiceItem) => { | |
item.description?.toLowerCase().includes("environmental fee")) | |
); | |
}; | |
+ | |
+export const isSuspectedError = (item: InvoiceItem) => { | |
+ const netUnitCost = getNetUnitCost(item); | |
+ const priceDiff = Math.abs( | |
+ (100 * ((item.sellPrice ?? 0) - (netUnitCost ?? 0))) / | |
+ (item.sellPrice ?? 1), | |
+ ); | |
+ return priceDiff > 40; | |
+}; | |
+ | |
export interface InvoicePageTableProps { | |
isEditing: boolean; | |
setLinkingItem: React.Dispatch<React.SetStateAction<InvoiceItem | undefined>>; | |
@@ -81,6 +100,8 @@ const InvoicePageTableInner = ( | |
const invoiceRef = useRef(invoice); | |
const getTotalDiffRef = useRef(getTotalDiff); | |
const linkItemDisclosureRef = useRef(linkItemDisclosure); | |
+ const [selectedItem, setSelectedItem] = useState<ItemDetails>(); | |
+ | |
useEffect(() => { | |
setInvoiceItemPropertyRef.current = setInvoiceItemProperty; | |
sumsRef.current = sums; | |
@@ -102,7 +123,11 @@ const InvoicePageTableInner = ( | |
columnHelper.accessor("description", { | |
header: "Name", | |
cell: ({ getValue, row: { original: item } }) => ( | |
- <VStack alignItems="flex-start"> | |
+ <VStack | |
+ alignItems="flex-start" | |
+ as={item.item ? Link : undefined} | |
+ onClick={() => setSelectedItem(item.item)} | |
+ > | |
<Text>{getValue() ?? "-"}</Text> | |
{item.invoiceItemDescription && ( | |
<Tooltip | |
@@ -251,7 +276,19 @@ const InvoicePageTableInner = ( | |
header: "Net Unit Cost", | |
meta: { headerProps: { textAlign: "right" } }, | |
cell: ({ row: { original: item } }) => ( | |
- <CurrencyRow value={getNetUnitCost(item)} /> | |
+ <HStack> | |
+ <CurrencyRow value={getNetUnitCost(item)} /> | |
+ {isSuspectedError(item) && ( | |
+ <Tooltip | |
+ label={ | |
+ "The net unit cost of this item is suspected to be incorrect because it varies from " + | |
+ "the sell price greatly. Please confirm the Units Per Case and cost are correct." | |
+ } | |
+ > | |
+ <WarningIcon color={"red"} /> | |
+ </Tooltip> | |
+ )} | |
+ </HStack> | |
), | |
}), | |
columnHelper.accessor( | |
@@ -469,6 +506,13 @@ const InvoicePageTableInner = ( | |
}} | |
containerProps={{ whiteSpace: "normal", w: "100%" }} | |
/> | |
+ {selectedItem && ( | |
+ <QuickItemEditModal | |
+ isOpen={!!selectedItem} | |
+ onClose={() => setSelectedItem(undefined)} | |
+ selectedItem={selectedItem} | |
+ /> | |
+ )} | |
</Box> | |
); | |
}; | |
diff --git a/src/pages/invoices/InvoicePage/InvoicePage.tsx b/src/pages/invoices/InvoicePage/InvoicePage.tsx | |
index 875a1fb..7b5e676 100644 | |
--- a/src/pages/invoices/InvoicePage/InvoicePage.tsx | |
+++ b/src/pages/invoices/InvoicePage/InvoicePage.tsx | |
@@ -69,7 +69,7 @@ const InvoicePage = () => { | |
}); | |
} | |
if (step === -1) { | |
- navigate("../"); | |
+ navigate(-1); | |
} else { | |
setActiveStep(step); | |
} | |
diff --git a/src/pages/invoices/InvoicePage/InvoicePricingStep/InvoicePricingTable.tsx b/src/pages/invoices/InvoicePage/InvoicePricingStep/InvoicePricingTable.tsx | |
index bf436d2..b7a5eed 100644 | |
--- a/src/pages/invoices/InvoicePage/InvoicePricingStep/InvoicePricingTable.tsx | |
+++ b/src/pages/invoices/InvoicePage/InvoicePricingStep/InvoicePricingTable.tsx | |
@@ -167,7 +167,7 @@ export const InvoicePricingTable: React.FC<InvoicePricingTableProps> = ({ | |
return [ | |
columnHelper.accessor("description", { | |
id: "description", | |
- header: "Description", | |
+ header: "Name", | |
cell: ({ getValue, row }) => { | |
return ( | |
<HStack | |
diff --git a/src/pages/invoices/InvoicesPage/InvoicesPage.tsx b/src/pages/invoices/InvoicesPage/InvoicesPage.tsx | |
index 127c512..ebab50e 100644 | |
--- a/src/pages/invoices/InvoicesPage/InvoicesPage.tsx | |
+++ b/src/pages/invoices/InvoicesPage/InvoicesPage.tsx | |
@@ -1,4 +1,4 @@ | |
-import { EditIcon } from "@chakra-ui/icons"; | |
+import { DeleteIcon, EditIcon } from "@chakra-ui/icons"; | |
import { | |
Button, | |
ButtonGroup, | |
@@ -26,9 +26,9 @@ import { FaSave } from "react-icons/fa"; | |
import { Link, useNavigate } from "react-router-dom"; | |
import { ArrayParam, useQueryParams, withDefault } from "use-query-params"; | |
import { PermissionedButton } from "../../../components/Auth/PermissionedButton"; | |
+import { PermissionedIconButton } from "../../../components/Auth/PermissionedIconButton"; | |
import { EditableInput } from "../../../components/common/EditableInput/EditableInput"; | |
-import { RadioCard } from "../../../components/common/RadioCard/RadioCard"; | |
-import { RadioCardGroup } from "../../../components/common/RadioCard/RadioCardGroup"; | |
+import { RadioCardV1 } from "../../../components/common/RadioCard/RadioCardV1"; | |
import { InvoiceStatusTag } from "../../../components/invoices/InvoiceStatusTag"; | |
import { NavBarPlain } from "../../../components/navbar/NavBarPlain"; | |
import { CurrencyRow } from "../../../components/Table/CurrencyRow"; | |
@@ -42,6 +42,7 @@ import { | |
InvoiceStatus, | |
InvoiceTree, | |
} from "../../../model/data-contracts"; | |
+import { arraysEqual } from "../../../utils/arrayUtils"; | |
import { CreateInvoice, CreateInvoiceFormProps } from "./CreateInvoiceModal"; | |
export const useListInvoices = () => { | |
@@ -52,11 +53,10 @@ export const useListInvoices = () => { | |
const [filters, setFilters] = useState<{ | |
status?: InvoiceStatus[]; | |
}>({}); | |
- const [, setUrlFilters] = useQueryParams({ | |
+ const [urlFilters, setUrlFilters] = useQueryParams({ | |
status: withDefault(ArrayParam, [ | |
InvoiceStatus.RECEIVING, | |
InvoiceStatus.PENDING_REVIEW, | |
- InvoiceStatus.COMPLETE, | |
InvoiceStatus.ITEM_MANAGEMENT, | |
]), | |
}); | |
@@ -64,16 +64,39 @@ export const useListInvoices = () => { | |
sort: [ | |
{ | |
id: "invoiceDate", | |
- desc: false, | |
+ desc: true, | |
}, | |
], | |
}); | |
+ const updateFilters = (filtersNew: { | |
+ status: InvoiceStatus[] | undefined; | |
+ }) => { | |
+ if ( | |
+ filtersNew.status && | |
+ filters.status && | |
+ !arraysEqual(filtersNew.status, filters.status) | |
+ ) { | |
+ setUrlFilters({ | |
+ status: filtersNew.status, | |
+ }); | |
+ } | |
+ }; | |
useEffect(() => { | |
- setUrlFilters({ | |
- status: filters.status, | |
- }); | |
- }, [filters.status, setUrlFilters]); | |
+ if (!filters.status || !arraysEqual(filters.status, urlFilters.status)) { | |
+ setFilters({ | |
+ status: urlFilters.status | |
+ .filter((v) => v != null) | |
+ .map((v) => v! as InvoiceStatus), | |
+ }); | |
+ setPageState((prev) => { | |
+ return { | |
+ ...prev, | |
+ number: 1, | |
+ }; | |
+ }); | |
+ } | |
+ }, [urlFilters.status]); | |
useEffect(() => { | |
if (!invoiceApi) return; | |
@@ -113,6 +136,7 @@ export const useListInvoices = () => { | |
loading, | |
filters, | |
setFilters, | |
+ updateFilters, | |
}; | |
}; | |
@@ -122,7 +146,7 @@ const statusFromFilters = ( | |
filters: (string | null)[], | |
): "All" | "Incomplete" | "Complete" => { | |
const nonNullFilters = filters.filter((f) => f !== null) as InvoiceStatus[]; | |
- if (nonNullFilters.length === 0) return "All"; | |
+ if (nonNullFilters.length === 0) return "Incomplete"; | |
if (nonNullFilters.length === 1) { | |
if (nonNullFilters[0] === InvoiceStatus.COMPLETE) return "Complete"; | |
} | |
@@ -132,7 +156,13 @@ const statusFromFilters = ( | |
}; | |
const filtersFromStatus = (value: string) => { | |
- if (value === "All") return undefined; | |
+ if (value === "All") | |
+ return [ | |
+ InvoiceStatus.COMPLETE, | |
+ InvoiceStatus.RECEIVING, | |
+ InvoiceStatus.PENDING_REVIEW, | |
+ InvoiceStatus.ITEM_MANAGEMENT, | |
+ ]; | |
if (value === "Complete") return [InvoiceStatus.COMPLETE]; | |
if (value === "Incomplete") | |
return [ | |
@@ -154,19 +184,20 @@ const InvoicesPage = () => { | |
setSortState, | |
loading, | |
filters, | |
- setFilters, | |
+ updateFilters, | |
} = useListInvoices(); | |
const createInvoiceModal = useDisclosure(); | |
const colorMode = useColorModeValue("blue.200", "blue.700"); | |
const vendors = useAllVendors(); | |
const showSaveButton = useSet<Invoice["id"]>([]); | |
const toast = useToast(); | |
+ const [isDeleting, setIsDeleting] = useState(false); | |
const { getRadioProps } = useRadioGroup({ | |
name: "groups", | |
- defaultValue: "All", | |
- value: filters.status ? statusFromFilters(filters.status) : "All", | |
+ defaultValue: "Incomplete", | |
+ value: filters.status ? statusFromFilters(filters.status) : "Incomplete", | |
onChange: (status) => { | |
- setFilters({ status: filtersFromStatus(status) }); | |
+ updateFilters({ status: filtersFromStatus(status) }); | |
}, | |
}); | |
@@ -310,7 +341,7 @@ const InvoicesPage = () => { | |
<> | |
<ButtonGroup> | |
<ChakraLink as={Link} to={`/invoices/${row.original.id}/`}> | |
- <Button colorScheme={"green"} leftIcon={<EditIcon />}> | |
+ <Button colorScheme={"blue"} leftIcon={<EditIcon />}> | |
Open | |
</Button> | |
</ChakraLink> | |
@@ -340,6 +371,35 @@ const InvoicesPage = () => { | |
Save | |
</Button> | |
)} | |
+ <PermissionedIconButton | |
+ requires={"invoice:manage"} | |
+ colorScheme={"blue"} | |
+ isLoading={isDeleting} | |
+ aria-label={"Delete invoice"} | |
+ icon={<DeleteIcon />} | |
+ onClick={() => { | |
+ if (!invoiceApi) return; | |
+ if (!entity) return; | |
+ setIsDeleting(true); | |
+ invoiceApi | |
+ .invoiceApiDelete(row.original.id) | |
+ .then(() => { | |
+ setInvoices((prev) => { | |
+ return prev.filter((i) => i.id !== row.original.id); | |
+ }); | |
+ toast({ | |
+ title: "Invoice deleted.", | |
+ description: "Invoice deleted successfully.", | |
+ status: "success", | |
+ duration: 5000, | |
+ isClosable: true, | |
+ }); | |
+ }) | |
+ .finally(() => { | |
+ setIsDeleting(false); | |
+ }); | |
+ }} | |
+ /> | |
</ButtonGroup> | |
</> | |
), | |
@@ -407,13 +467,16 @@ const InvoicesPage = () => { | |
<HStack> | |
<Heading size="md">Invoices</Heading> | |
<Divider orientation="vertical" /> | |
- <RadioCardGroup direction="row"> | |
- {["All", "Incomplete", "Complete"].map((value) => ( | |
- <RadioCard key={value} value={value}> | |
- {value} | |
- </RadioCard> | |
- ))} | |
- </RadioCardGroup> | |
+ <HStack {...getRadioProps()}> | |
+ {["Incomplete", "Complete", "All"].map((value) => { | |
+ const radio = getRadioProps({ value }); | |
+ return ( | |
+ <RadioCardV1 key={value} {...radio}> | |
+ {value} | |
+ </RadioCardV1> | |
+ ); | |
+ })} | |
+ </HStack> | |
{loading && <Spinner ml="2" size="sm" />} | |
</HStack> | |
<Spacer /> | |
@@ -423,7 +486,7 @@ const InvoicesPage = () => { | |
createInvoiceModal.onOpen(); | |
}} | |
isLoading={createInvoiceModal.isOpen} | |
- colorScheme={"green"} | |
+ colorScheme={"blue"} | |
> | |
Create Invoice | |
</PermissionedButton> | |
@@ -433,6 +496,11 @@ const InvoicesPage = () => { | |
<TransformityDataTable | |
containerProps={{ | |
w: "100%", | |
+ justifyContent: "space-between", | |
+ minH: "100%", | |
+ flexGrow: 2, | |
+ as: Flex, | |
+ flexDirection: "column", | |
}} | |
key={pageState.number} | |
serverSideSort | |
@@ -461,7 +529,7 @@ const InvoicesPage = () => { | |
initialSortingState={[ | |
{ | |
id: "invoiceDate", | |
- desc: false, | |
+ desc: true, | |
}, | |
]} | |
/> | |
diff --git a/src/pages/promotions/CreatePromotionsModal.tsx b/src/pages/promotions/CreatePromotionsModal.tsx | |
index d1837d3..ced29e6 100644 | |
--- a/src/pages/promotions/CreatePromotionsModal.tsx | |
+++ b/src/pages/promotions/CreatePromotionsModal.tsx | |
@@ -1,5 +1,6 @@ | |
import { useToast } from "@chakra-ui/react"; | |
import * as Sentry from "@sentry/react"; | |
+import moment from "moment"; | |
import { useCallback } from "react"; | |
import { BasicModal } from "../../components/common/BasicModal"; | |
import { PromotionsForm } from "../../components/promotions/PromotionsForm"; | |
@@ -23,8 +24,15 @@ export const CreatePromotionsModal: React.FC<CreatePromotionsModalProps> = ({ | |
async (promotion: CreatePromotionTree) => { | |
if (promotionApi) { | |
try { | |
- await promotionApi.createPromotion(promotion); | |
+ await promotionApi.createPromotion({ | |
+ ...promotion, | |
+ startDate: moment(promotion.startDate).startOf("day").toISOString(), | |
+ endDate: promotion.endDate | |
+ ? moment(promotion.endDate).startOf("day").toISOString() | |
+ : undefined, | |
+ }); | |
disclosure.onClose(); | |
+ window.location.reload(); | |
} catch (e) { | |
console.error(e); | |
toast({ | |
@@ -42,7 +50,7 @@ export const CreatePromotionsModal: React.FC<CreatePromotionsModalProps> = ({ | |
); | |
return ( | |
- <BasicModal width="5xl" title="Create Promotion" disclosure={disclosure}> | |
+ <BasicModal width="full" title="Create Promotion" disclosure={disclosure}> | |
<PromotionsForm onCancel={disclosure.onClose} onSubmit={onSubmit} /> | |
</BasicModal> | |
); | |
diff --git a/src/pages/promotions/PromotionDetails.tsx b/src/pages/promotions/PromotionDetails.tsx | |
index f6b2f5a..515f0be 100644 | |
--- a/src/pages/promotions/PromotionDetails.tsx | |
+++ b/src/pages/promotions/PromotionDetails.tsx | |
@@ -19,6 +19,7 @@ import { | |
useToast, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import { DatePicker, LineChart } from "@tremor/react"; | |
import _ from "lodash"; | |
import moment from "moment"; | |
@@ -46,6 +47,7 @@ import { | |
matcherToPatchPromotionItem, | |
promotionItemToPatch, | |
} from "../../utils/PromotionsUtils"; | |
+ | |
export type PromotionDetailsProps = {}; | |
const PromotionDetails: React.FC<PromotionDetailsProps> = () => { | |
@@ -82,6 +84,7 @@ const PromotionDetails: React.FC<PromotionDetailsProps> = () => { | |
description: e.message, | |
status: "error", | |
}); | |
+ Sentry.captureException(e); | |
} | |
setIsLoading(false); | |
}, | |
@@ -109,6 +112,7 @@ const PromotionDetails: React.FC<PromotionDetailsProps> = () => { | |
description: e.message, | |
status: "error", | |
}); | |
+ Sentry.captureException(e); | |
} | |
setIsLoading(false); | |
}; | |
@@ -162,14 +166,16 @@ const PromotionDetails: React.FC<PromotionDetailsProps> = () => { | |
<Button | |
bg="white" | |
colorScheme="red" | |
+ data-testid="deactivate-promotion" | |
isDisabled={Boolean( | |
promotion?.endDate && moment(promotion.endDate).isBefore(), | |
)} | |
leftIcon={<BiArchiveIn />} | |
onClick={() => { | |
if (!promotion) return; | |
- const deactivated = { | |
+ const deactivated: PromotionTree = { | |
...promotion, | |
+ startDate: moment().subtract(1, "day").format("YYYY-MM-DD"), | |
endDate: moment().subtract(1, "day").format("YYYY-MM-DD"), | |
}; | |
setPromotion(deactivated); | |
@@ -288,7 +294,11 @@ const PromotionDetails: React.FC<PromotionDetailsProps> = () => { | |
)} | |
</HStack> | |
<HStack w="100%" justifyContent="space-between"> | |
- <VStack w="15%" alignItems="flex-start"> | |
+ <VStack | |
+ data-testid="promotion-dates" | |
+ w="15%" | |
+ alignItems="flex-start" | |
+ > | |
<Stat> | |
<StatLabel>Start Date</StatLabel> | |
{isEditing ? ( | |
@@ -338,7 +348,7 @@ const PromotionDetails: React.FC<PromotionDetailsProps> = () => { | |
</VStack> | |
<Divider orientation="vertical" /> | |
<VStack w="100%"> | |
- <HStack w="100%" alignItems="flex-start"> | |
+ <HStack w="100%" alignItems="stretch"> | |
<VStack w="100%" alignItems="flex-start"> | |
<Text fontWeight="medium">Included Items</Text> | |
<PromotionMatcherForm | |
diff --git a/src/pages/promotions/PromotionsDashboard.tsx b/src/pages/promotions/PromotionsDashboard.tsx | |
index 84a58e9..b5d6fed 100644 | |
--- a/src/pages/promotions/PromotionsDashboard.tsx | |
+++ b/src/pages/promotions/PromotionsDashboard.tsx | |
@@ -13,8 +13,9 @@ import { | |
useToast, | |
VStack, | |
} from "@chakra-ui/react"; | |
+import * as Sentry from "@sentry/react"; | |
import _ from "lodash"; | |
-import { useEffect, useState } from "react"; | |
+import { useEffect, useMemo, useState } from "react"; | |
import { | |
createEnumParam, | |
InjectedQueryProps, | |
@@ -34,7 +35,6 @@ import { | |
PromotionStatus, | |
PromotionTree, | |
} from "../../model/data-contracts"; | |
-import { serializeColumnSort } from "../../utils/reportUtils"; | |
import { convertUpperSnakeToNormalCase } from "../../utils/stringUtils"; | |
import { CreatePromotionsModal } from "./CreatePromotionsModal"; | |
@@ -54,7 +54,7 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
query, | |
setQuery, | |
}) => { | |
- const { page, sort, status } = query; | |
+ const { page, status } = query; | |
const [entity] = useEntitySelected(); | |
const promotionApi = usePromotionApi(); | |
const toast = useToast(); | |
@@ -64,6 +64,8 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
useState<PromotionTree[]>(); | |
const [isLoading, setIsLoading] = useState(false); | |
const [totalPages, setTotalPages] = useState(1); | |
+ const pageMemo = useMemo(() => page, [page]); | |
+ const statusMemo = useMemo(() => status, [status]); | |
// Effect to fetch promotions list | |
useEffect(() => { | |
@@ -74,10 +76,10 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
setIsLoading(true); | |
const response = await promotionApi.listPromotions({ | |
entityId: entity.id, | |
- page: page - 1, | |
+ page: pageMemo - 1, | |
size: 10, | |
- sort: serializeColumnSort(sort), | |
- status: status, | |
+ status: statusMemo, | |
+ zoneId: Intl.DateTimeFormat().resolvedOptions().timeZone, | |
}); | |
setTotalPages(response.data.page.totalPages); | |
setQuery((prev) => ({ ...prev, page: response.data.page.number + 1 })); | |
@@ -90,12 +92,14 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
duration: 3000, | |
isClosable: true, | |
}); | |
+ Sentry.captureException(e); | |
+ } finally { | |
+ setIsLoading(false); | |
} | |
- setIsLoading(false); | |
}; | |
getPromotions(); | |
- }, [promotionApi, entity?.id, toast, page, status, sort, setQuery]); | |
+ }, [promotionApi, entity?.id, toast, pageMemo, statusMemo, setQuery]); | |
// Effect to fetch promotions stats | |
useEffect(() => { | |
@@ -118,6 +122,7 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
duration: 3000, | |
isClosable: true, | |
}); | |
+ Sentry.captureException(e); | |
} | |
}; | |
@@ -133,6 +138,7 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
<Button | |
leftIcon={<AddIcon />} | |
colorScheme="blue" | |
+ data-testid="create-promotion-button" | |
onClick={createPromotionsDisclosure.onOpen} | |
> | |
Create New | |
@@ -142,7 +148,7 @@ const PromotionsDashboard: React.FC<PromotionsDashboardProps> = ({ | |
<Card p={2} w="100%"> | |
<Stat> | |
<StatLabel>Active Promotions</StatLabel> | |
- <StatNumber> | |
+ <StatNumber data-testid="active-promotions"> | |
{Object.values(_.groupBy(stats, (stat) => stat.id)).length} | |
</StatNumber> | |
<StatHelpText>No. of currently active promotions</StatHelpText> | |
diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts | |
index 9cbcfea..29543f8 100644 | |
--- a/src/utils/arrayUtils.ts | |
+++ b/src/utils/arrayUtils.ts | |
@@ -5,3 +5,11 @@ export function removeElementFromArray<T>(arr: T[], elementToRemove: T): void { | |
} | |
} | |
} | |
+ | |
+export function arraysEqual<T>(arr1: T[], arr2: T[]): boolean { | |
+ return ( | |
+ arr1.length === arr2.length && | |
+ arr2.every((v) => arr1.includes(v)) && | |
+ arr1.every((v) => arr2.includes(v)) | |
+ ); | |
+} | |
diff --git a/src/utils/numberUtils.ts b/src/utils/numberUtils.ts | |
index 2fbbf87..6aaac45 100644 | |
--- a/src/utils/numberUtils.ts | |
+++ b/src/utils/numberUtils.ts | |
@@ -1,3 +1,5 @@ | |
+import { TransactionData } from "../model/data-contracts"; | |
+ | |
export function roundToNearestNine(value: number): number { | |
// Multiply by 100 to work with the hundredths place | |
const multipliedValue = value * 100; | |
@@ -8,3 +10,7 @@ export function roundToNearestNine(value: number): number { | |
// Divide by 100 to get the original scale back | |
return roundedValue / 100; | |
} | |
+ | |
+export function calculateSubtotal(transaction: TransactionData): number { | |
+ return transaction.price * transaction.quantity - transaction.discount; | |
+} | |
diff --git a/src/utils/stringUtils.tsx b/src/utils/stringUtils.tsx | |
index 62a69bb..a853443 100644 | |
--- a/src/utils/stringUtils.tsx | |
+++ b/src/utils/stringUtils.tsx | |
@@ -91,10 +91,10 @@ export function getUnitCost(totalCost: number, unitsEntered: number) { | |
return totalCost / unitsEntered; | |
} | |
-export function getProfitMargin(item: PurchaseOrderItemDetails) { | |
- const sellPrice = item.item.sellPrice; | |
- if (item.totalCost && sellPrice) { | |
- const unitCost = getUnitCost(item.totalCost, item.unitsEntered); | |
+export function getProfitMargin(lineItem: PurchaseOrderItemDetails) { | |
+ const sellPrice = lineItem.item.sellPrice; | |
+ if (lineItem.totalCost && sellPrice) { | |
+ const unitCost = getUnitCost(lineItem.totalCost, lineItem.unitsEntered); | |
return `${(((sellPrice - unitCost) / sellPrice) * 100).toFixed(2)} %`; | |
} | |
} | |
diff --git a/tailwind.config.js b/tailwind.config.js | |
index ef81867..af8b4a7 100644 | |
--- a/tailwind.config.js | |
+++ b/tailwind.config.js | |
@@ -2,6 +2,7 @@ | |
const colors = require("tailwindcss/colors"); | |
module.exports = { | |
+ darkMode: "class", | |
content: [ | |
"./src/**/*.{js,ts,jsx,tsx}", | |
"./node_modules/@tremor/**/*.{js,ts,jsx,tsx}", |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment