Skip to content

Instantly share code, notes, and snippets.

@jay-babu
Created February 13, 2024 04:22
Show Gist options
  • Save jay-babu/feb3c93d34c50d67963c74c1afccc962 to your computer and use it in GitHub Desktop.
Save jay-babu/feb3c93d34c50d67963c74c1afccc962 to your computer and use it in GitHub Desktop.
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