Merged in test-beta (pull request #1314)

Test beta
This commit is contained in:
Dave Richer
2024-03-01 23:11:42 +00:00
24 changed files with 2385 additions and 1486 deletions

16
.prettierrc.js Normal file
View File

@@ -0,0 +1,16 @@
exports.default = {
printWidth: 120,
useTabs: false,
tabWidth: 2,
trailingComma: "es5",
semi: true,
singleQuote: false,
bracketSpacing: true,
arrowParens: "always",
jsxSingleQuote: false,
bracketSameLine: false,
endOfLine: "lf",
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
importOrderSeparation: true,
importOrderSortSpecifiers: true,
};

View File

@@ -3,7 +3,13 @@
This documentation details the schema required for `.filters` files on the report server. It is used to dynamically This documentation details the schema required for `.filters` files on the report server. It is used to dynamically
modify the graphQL query and provide the user more power over their reports. modify the graphQL query and provide the user more power over their reports.
# Special Notes For filters and sorters, valid types include (`type` key in the schema):
- string (default)
- number
- bool or boolean
- date
## Special Notes
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected - When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
## High level Schema Overview ## High level Schema Overview
@@ -40,9 +46,10 @@ Filters effect the where clause of the graphQL query. They are used to filter th
A note on special notation used in the `name` field. A note on special notation used in the `name` field.
## Reflection ## Reflection
Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file. Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file.
``` ```json
{ {
"name": "jobs.status", "name": "jobs.status",
"translation": "jobs.fields.status", "translation": "jobs.fields.status",
@@ -67,6 +74,8 @@ The following cases are available
- `special.employees` - This will reflect the employees `bodyshop.employees` - `special.employees` - This will reflect the employees `bodyshop.employees`
- `special.first_names` - This will reflect the first names `bodyshop.employees` - `special.first_names` - This will reflect the first names `bodyshop.employees`
- `special.last_names` - This will reflect the last names `bodyshop.employees` - `special.last_names` - This will reflect the last names `bodyshop.employees`
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources
- `special.class`- This will reflect the class `bodyshop.md_classes`
- -
### Path without brackets, multi level ### Path without brackets, multi level
@@ -142,8 +151,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
- Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs` - Will only support two level of nesting in the graphQL query `jobs.joblines.mod_lb_hrs` vs `[jobs].joblines.mod_lb_hrs`
is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not. is fine, but `jobs.[joblines.].some_table.mod_lb_hrs` is not.
- The `dates` object is not yet implemented and will be added in a future release. - The type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' and is case-sensitive.
- The type object must be 'string' or 'number' and is case-sensitive.
- The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used. - The `translation` key is used to look up the label in the GUI, if it is not found, the `label` key is used.
- Do not add the ability to filter things that are already filtered as part of the original query, this would be - Do not add the ability to filter things that are already filtered as part of the original query, this would be
redundant and could cause issues. redundant and could cause issues.
@@ -158,6 +166,7 @@ query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!
using the sorters. using the sorters.
### Default Sorters ### Default Sorters
- A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves. - A sorter can be given a default object containing a `order` and `direction` key value. This will be used to sort the report if the user does not select any of the sorters themselves.
- The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`. - The `order` key is the order in which the sorters are applied, and the `direction` key is the direction of the sort, either `asc` or `desc`.

513
client/package-lock.json generated
View File

@@ -17,8 +17,8 @@
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.1", "@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6", "@sentry/cli": "^2.28.6",
"@sentry/react": "^7.102.1", "@sentry/react": "^7.104.0",
"@sentry/tracing": "^7.102.1", "@sentry/tracing": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0", "@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51", "@tanem/react-nprogress": "^5.0.51",
"antd": "^5.14.2", "antd": "^5.14.2",
@@ -33,7 +33,7 @@
"enquire-js": "^0.2.1", "enquire-js": "^0.2.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^10.8.0", "firebase": "^10.8.1",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"i18next": "^23.10.0", "i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2", "i18next-browser-languagedetector": "^7.0.2",
@@ -46,11 +46,11 @@
"phone": "^3.1.42", "phone": "^3.1.42",
"preval.macro": "^5.0.0", "preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^8.2.0", "query-string": "^9.0.0",
"rc-queue-anim": "^2.0.0", "rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6", "rc-scroll-anim": "^2.7.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-big-calendar": "^1.10.3", "react-big-calendar": "^1.11.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^7.1.0", "react-cookie": "^7.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -62,15 +62,15 @@
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.8.1", "react-intersection-observer": "^9.8.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-number-format": "^5.1.4", "react-number-format": "^5.3.3",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-router-dom": "^6.22.1", "react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5", "react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"recharts": "^2.12.1", "recharts": "^2.12.2",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.3.0", "redux-saga": "^1.3.0",
@@ -88,7 +88,7 @@
"workbox-precaching": "^7.0.0", "workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0", "workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0", "workbox-strategies": "^7.0.0",
"yauzl": "^3.1.0" "yauzl": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
@@ -2991,9 +2991,9 @@
} }
}, },
"node_modules/@fastify/busboy": { "node_modules/@fastify/busboy": {
"version": "2.1.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
} }
@@ -3042,9 +3042,9 @@
"integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw=="
}, },
"node_modules/@firebase/app": { "node_modules/@firebase/app": {
"version": "0.9.27", "version": "0.9.28",
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.27.tgz", "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.28.tgz",
"integrity": "sha512-p2Dvl1ge4kRsyK5+wWcmdAIE9MSwZ0pDKAYB51LZgZuz6wciUZk4E1yAEdkfQlRxuHehn+Ol9WP5Qk2XQZiHGg==", "integrity": "sha512-MS0+EtNixrwJbVDs5Bt/lhUhzeWGUtUoP6X+zYZck5GAZwI5g4F91noVA9oIXlFlpn6Q1xIbiaHA2GwGk7/7Ag==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/logger": "0.4.0", "@firebase/logger": "0.4.0",
@@ -3094,11 +3094,11 @@
"integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ=="
}, },
"node_modules/@firebase/app-compat": { "node_modules/@firebase/app-compat": {
"version": "0.2.27", "version": "0.2.28",
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.27.tgz", "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.28.tgz",
"integrity": "sha512-SYlqocfUDKPHR6MSFC8hree0BTiWFu5o8wbf6zFlYXyG41w7TcHp4wJi4H/EL5V6cM4kxwruXTJtqXX/fRAZtw==", "integrity": "sha512-Mr2NbeM1Oaayuw5unUAMzt+7/MN+e2uklT1l87D+ZLJl2UvhZAZmMt74GjEI9N3sDYKMeszSbszBqtJ1fGVafQ==",
"dependencies": { "dependencies": {
"@firebase/app": "0.9.27", "@firebase/app": "0.9.28",
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/logger": "0.4.0", "@firebase/logger": "0.4.0",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
@@ -3111,15 +3111,15 @@
"integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q==" "integrity": "sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q=="
}, },
"node_modules/@firebase/auth": { "node_modules/@firebase/auth": {
"version": "1.6.0", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.6.0.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.6.1.tgz",
"integrity": "sha512-Qhl35eJTV6BwvuueTPCY6x8kUlYyzALtjp/Ws0X3fw3AnjVVfuVb7oQ3Xh5VPVfMFhaIuUAd1KXwcAuIklkSDw==", "integrity": "sha512-oOuQVOxtxKr+kTTqEkkI2qXIeGbkNLpA8FzO030LF4KXmMcETqsPaIqw7Aw1Y4Zl82l1qpZtpc4vN4Da2qZdfQ==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/logger": "0.4.0", "@firebase/logger": "0.4.0",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"undici": "5.26.5" "undici": "5.28.3"
}, },
"peerDependencies": { "peerDependencies": {
"@firebase/app": "0.x", "@firebase/app": "0.x",
@@ -3132,16 +3132,16 @@
} }
}, },
"node_modules/@firebase/auth-compat": { "node_modules/@firebase/auth-compat": {
"version": "0.5.2", "version": "0.5.3",
"resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.3.tgz",
"integrity": "sha512-pRgje5BPCNR1vXyvGOVXwOHtv88A2WooXfklI8sV7/jWi03ExFqNfpJT26GUo/oD39NoKJ3Kt6rD5gVvdV7lMw==", "integrity": "sha512-2pVtVEvu8P7SF6jSPfLPKWUClQFj+StqAZ0fD/uQ6mv8DyWn7AuuANFEu7Pv96JPcaL6Gy9jC5dFqjpptjqSRA==",
"dependencies": { "dependencies": {
"@firebase/auth": "1.6.0", "@firebase/auth": "1.6.1",
"@firebase/auth-types": "0.12.0", "@firebase/auth-types": "0.12.0",
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"undici": "5.26.5" "undici": "5.28.3"
}, },
"peerDependencies": { "peerDependencies": {
"@firebase/app-compat": "0.x" "@firebase/app-compat": "0.x"
@@ -3207,9 +3207,9 @@
} }
}, },
"node_modules/@firebase/firestore": { "node_modules/@firebase/firestore": {
"version": "4.4.2", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.4.2.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.4.3.tgz",
"integrity": "sha512-YaX6ypa/RzU6OkxzUQlpSxwhOIWdTraCNz7sMsbaSEjjl/pj/QvX6TqjkdWGzuBYh2S6rz7ErhDO0g39oZZw/g==", "integrity": "sha512-Ix61zbeuTsHf0WFbk6+67n89Vzd9M8MMTdnz7c7z+BRE3BS5Vuc3gX5ZcHFjqPkQJ7rpLB1egHsYe4Przp5C2g==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/logger": "0.4.0", "@firebase/logger": "0.4.0",
@@ -3218,7 +3218,7 @@
"@grpc/grpc-js": "~1.9.0", "@grpc/grpc-js": "~1.9.0",
"@grpc/proto-loader": "^0.7.8", "@grpc/proto-loader": "^0.7.8",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"undici": "5.26.5" "undici": "5.28.3"
}, },
"engines": { "engines": {
"node": ">=10.10.0" "node": ">=10.10.0"
@@ -3228,12 +3228,12 @@
} }
}, },
"node_modules/@firebase/firestore-compat": { "node_modules/@firebase/firestore-compat": {
"version": "0.3.25", "version": "0.3.26",
"resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.25.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.26.tgz",
"integrity": "sha512-+xI7WmsgZCBhMn/+uhDKcg+lsOUJ9FJyt5PGTzkFPbCsozWfeQZ7eVnfPh0rMkUOf0yIQ924RIe04gwvEIbcoQ==", "integrity": "sha512-dNrKiH5Cn6ItANV9nJI2Y0msKBj/skO7skDlRo/BUSQE1DKbNzumxpJEz+PK/PV1nTegnRgVvs47gpQeVWXtYQ==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/firestore": "4.4.2", "@firebase/firestore": "4.4.3",
"@firebase/firestore-types": "3.0.0", "@firebase/firestore-types": "3.0.0",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0" "tslib": "^2.1.0"
@@ -3252,9 +3252,9 @@
} }
}, },
"node_modules/@firebase/functions": { "node_modules/@firebase/functions": {
"version": "0.11.1", "version": "0.11.2",
"resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.1.tgz", "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.2.tgz",
"integrity": "sha512-3uUa1hB79Gmy6E1gHTfzoHeZolBeHc/I/n3+lOCDe6BOos9AHmzRjKygcFE/7VA2FJjitCE0K+OHI6+OuoY8fQ==", "integrity": "sha512-2NULTYOZbu0rXczwfYdqQH0w1FmmYrKjTy1YPQSHLCAkMBdfewoKmVm4Lyo2vRn0H9ZndciLY7NszKDFt9MKCQ==",
"dependencies": { "dependencies": {
"@firebase/app-check-interop-types": "0.3.0", "@firebase/app-check-interop-types": "0.3.0",
"@firebase/auth-interop-types": "0.2.1", "@firebase/auth-interop-types": "0.2.1",
@@ -3262,19 +3262,19 @@
"@firebase/messaging-interop-types": "0.2.0", "@firebase/messaging-interop-types": "0.2.0",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"undici": "5.26.5" "undici": "5.28.3"
}, },
"peerDependencies": { "peerDependencies": {
"@firebase/app": "0.x" "@firebase/app": "0.x"
} }
}, },
"node_modules/@firebase/functions-compat": { "node_modules/@firebase/functions-compat": {
"version": "0.3.7", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.7.tgz", "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.8.tgz",
"integrity": "sha512-uXe6Kmku5lNogp3OpPBcOJbSvnaCOn+YxS3zlXKNU6Q/NLwcvO3RY1zwYyctCos2RemEw3KEQ7YdzcECXjHWLw==", "integrity": "sha512-VDHSw6UOu8RxfgAY/q8e+Jn+9Fh60Fc28yck0yfMsi2e0BiWgonIMWkFspFGGLgOJebTHl+hc+9v91rhzU6xlg==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/functions": "0.11.1", "@firebase/functions": "0.11.2",
"@firebase/functions-types": "0.6.0", "@firebase/functions-types": "0.6.0",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0" "tslib": "^2.1.0"
@@ -3441,26 +3441,26 @@
"integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA=="
}, },
"node_modules/@firebase/storage": { "node_modules/@firebase/storage": {
"version": "0.12.1", "version": "0.12.2",
"resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.1.tgz", "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.2.tgz",
"integrity": "sha512-KJ5NV7FUh54TeTlEjdkTTX60ciCKOp9EqlbLnpdcXUYRJg0Z4810TXbilPc1z7fTIG4iPjtdi95bGE9n4dBX8A==", "integrity": "sha512-MzanOBcxDx9oOwDaDPMuiYxd6CxcN1xZm+os5uNE3C1itbRKLhM9rzpODDKWzcbnHHFtXk3Q3lsK/d3Xa1WYYw==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0", "tslib": "^2.1.0",
"undici": "5.26.5" "undici": "5.28.3"
}, },
"peerDependencies": { "peerDependencies": {
"@firebase/app": "0.x" "@firebase/app": "0.x"
} }
}, },
"node_modules/@firebase/storage-compat": { "node_modules/@firebase/storage-compat": {
"version": "0.3.4", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.4.tgz", "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.5.tgz",
"integrity": "sha512-Y0m5e2gS/wB9Ioth2X/Sgz76vcxvqgQrCmfa9qwhss/N31kxY2Gks6Frv0nrE18AjVfcSmcfDitqUwxcMOTRSg==", "integrity": "sha512-5dJXfY5NxCF5NAk4dLvJqC+m6cgcf0Fr29nrMHwhwI34pBheQq2PdRZqALsqZCES9dnHTuFNlqGQDpLr+Ph4rw==",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.5", "@firebase/component": "0.6.5",
"@firebase/storage": "0.12.1", "@firebase/storage": "0.12.2",
"@firebase/storage-types": "0.8.0", "@firebase/storage-types": "0.8.0",
"@firebase/util": "1.9.4", "@firebase/util": "1.9.4",
"tslib": "^2.1.0" "tslib": "^2.1.0"
@@ -4394,9 +4394,9 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.15.1", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz",
"integrity": "sha512-zcU0gM3z+3iqj8UX45AmWY810l3oUmXM7uH4dt5xtzvMhRtYVhKGOmgOd1877dOPPepfCjUv57w+syamWIYe7w==", "integrity": "sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@@ -4500,89 +4500,40 @@
"integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==" "integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA=="
}, },
"node_modules/@sentry-internal/feedback": { "node_modules/@sentry-internal/feedback": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.104.0.tgz",
"integrity": "sha512-vY4hpLLMNLjICtWiizc7KeGbWOTUMGrF7C+9dPCztZww3CLgzWy9A7DvPj5hodRiYzpdRnAMl8yQnMFbYXh7bA==", "integrity": "sha512-+OWqm+X9ZfEQQmxVoZsc9lpzd85pabAT+bEj57StRMTnfdRbD9TippS20nCD9N2Ql5v2/41NfiPONMejGbnOwg==",
"dependencies": { "dependencies": {
"@sentry/core": "7.102.1", "@sentry/core": "7.104.0",
"@sentry/types": "7.102.1", "@sentry/types": "7.104.0",
"@sentry/utils": "7.102.1" "@sentry/utils": "7.104.0"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@sentry-internal/feedback/node_modules/@sentry/core": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
"dependencies": {
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/feedback/node_modules/@sentry/types": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
"dependencies": {
"@sentry/types": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas": { "node_modules/@sentry-internal/replay-canvas": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.104.0.tgz",
"integrity": "sha512-GUX4RWI10uRjdjeyvCLtAAhWRVqnAnG6+yNxWfqUQ3qMA7B7XxG43KT2UhSnulmErNzODQ6hA68rGPwwYeRIww==", "integrity": "sha512-gfdnkFIpxAveKNghkvRCqv+hSiBkxYVoyFZLTvUPuM9Cmvmket1/PpnuWMC2jNtCEewG3gxkPDd4EaT9oa1HZQ==",
"dependencies": { "dependencies": {
"@sentry/core": "7.102.1", "@sentry/core": "7.104.0",
"@sentry/replay": "7.102.1", "@sentry/replay": "7.104.0",
"@sentry/types": "7.102.1", "@sentry/types": "7.104.0",
"@sentry/utils": "7.102.1" "@sentry/utils": "7.104.0"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { "node_modules/@sentry-internal/tracing": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.104.0.tgz",
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==", "integrity": "sha512-2z7OijM1J5ndJUiJJElC3iH9qb/Eb8eYm2v8oJhM8WVdc5uCKfrQuYHNgGOnmY2FOCfEUlTmMQGpDw7DJ67L5w==",
"dependencies": { "dependencies": {
"@sentry/types": "7.102.1", "@sentry/core": "7.104.0",
"@sentry/utils": "7.102.1" "@sentry/types": "7.104.0",
}, "@sentry/utils": "7.104.0"
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
"dependencies": {
"@sentry/types": "7.102.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4598,61 +4549,17 @@
} }
}, },
"node_modules/@sentry/browser": { "node_modules/@sentry/browser": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.104.0.tgz",
"integrity": "sha512-7BOfPBiM7Kp6q/iy0JIbsBTxIASV+zWXByqqjuEMWGj3X2u4oRIfm3gv4erPU/l+CORQUVQZLSPGoIoM1gbB/A==", "integrity": "sha512-HsqO+mr1SowGoP0VbuWrQ2DZT0t5PLomy7LEYa6+4lbOemnY+5YV2NSwBTKbjYysvKipSwaRtPhXrsXsMaz8Bg==",
"dependencies": { "dependencies": {
"@sentry-internal/feedback": "7.102.1", "@sentry-internal/feedback": "7.104.0",
"@sentry-internal/replay-canvas": "7.102.1", "@sentry-internal/replay-canvas": "7.104.0",
"@sentry-internal/tracing": "7.102.1", "@sentry-internal/tracing": "7.104.0",
"@sentry/core": "7.102.1", "@sentry/core": "7.104.0",
"@sentry/replay": "7.102.1", "@sentry/replay": "7.104.0",
"@sentry/types": "7.102.1", "@sentry/types": "7.104.0",
"@sentry/utils": "7.102.1" "@sentry/utils": "7.104.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry-internal/tracing": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.1.tgz",
"integrity": "sha512-RkFlFyAC0fQOvBbBqnq0CLmFW5m3JJz9pKbZd5vXPraWAlniKSb1bC/4DF9SlNx0FN1LWG+IU3ISdpzwwTeAGg==",
"dependencies": {
"@sentry/core": "7.102.1",
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/core": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
"dependencies": {
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/types": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/browser/node_modules/@sentry/utils": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
"dependencies": {
"@sentry/types": "7.102.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -4858,15 +4765,27 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@sentry/react": { "node_modules/@sentry/core": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.104.0.tgz",
"integrity": "sha512-X4j2DgbktlEifnd21YJKCayAmff5hnaS+9MNz9OonEwD0ARi0ks7bo0wtWHMjPK20992MO+JwczVg/1BXJYDdQ==", "integrity": "sha512-XPndD6IGQGd07/EntvYVzOWQUo/Gd7L3DwYFeEKeBv6ByWjbBNmVZFRhU0GPPsCHKyW9yMU9OO9diLSS4ijsRg==",
"dependencies": { "dependencies": {
"@sentry/browser": "7.102.1", "@sentry/types": "7.104.0",
"@sentry/core": "7.102.1", "@sentry/utils": "7.104.0"
"@sentry/types": "7.102.1", },
"@sentry/utils": "7.102.1", "engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react": {
"version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.104.0.tgz",
"integrity": "sha512-JdPzX/rJ4sSr/pVFOKwVrUhr8McCn38w5Q+/wdCabO8fdUkoBe4P05LRCH4Rng0uOk8MeEQ+EvfMVB79DmxIgQ==",
"dependencies": {
"@sentry/browser": "7.104.0",
"@sentry/core": "7.104.0",
"@sentry/types": "7.104.0",
"@sentry/utils": "7.104.0",
"hoist-non-react-statics": "^3.3.2" "hoist-non-react-statics": "^3.3.2"
}, },
"engines": { "engines": {
@@ -4876,145 +4795,45 @@
"react": "15.x || 16.x || 17.x || 18.x" "react": "15.x || 16.x || 17.x || 18.x"
} }
}, },
"node_modules/@sentry/react/node_modules/@sentry/core": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
"dependencies": {
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react/node_modules/@sentry/types": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/react/node_modules/@sentry/utils": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
"dependencies": {
"@sentry/types": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay": { "node_modules/@sentry/replay": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.104.0.tgz",
"integrity": "sha512-HR/j9dGIvbrId8fh8mQlODx7JrhRmawEd9e9P3laPtogWCg/5TI+XPb2VGSaXOX9VWtb/6Z2UjHsaGjgg6YcuA==", "integrity": "sha512-HmWBr/u+SNeULxCxM8lJb2iqhjizeLGJtuKSShPEguEXIUT4kzdoqLh6wn7BAjiKzhmyjrnBcosR5LUqJtGYZQ==",
"dependencies": { "dependencies": {
"@sentry-internal/tracing": "7.102.1", "@sentry-internal/tracing": "7.104.0",
"@sentry/core": "7.102.1", "@sentry/core": "7.104.0",
"@sentry/types": "7.102.1", "@sentry/types": "7.104.0",
"@sentry/utils": "7.102.1" "@sentry/utils": "7.104.0"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@sentry/replay/node_modules/@sentry-internal/tracing": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.1.tgz",
"integrity": "sha512-RkFlFyAC0fQOvBbBqnq0CLmFW5m3JJz9pKbZd5vXPraWAlniKSb1bC/4DF9SlNx0FN1LWG+IU3ISdpzwwTeAGg==",
"dependencies": {
"@sentry/core": "7.102.1",
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/core": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz",
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==",
"dependencies": {
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/types": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/replay/node_modules/@sentry/utils": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
"dependencies": {
"@sentry/types": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/tracing": { "node_modules/@sentry/tracing": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.104.0.tgz",
"integrity": "sha512-9VQEox0R7ouhhUVHtBwlGlXG5beDCM/Uo0BY+G0M1H03aFJsLAwnxPNeWnK3WvPejxf94EgdimKMjDjv9l2Sbg==", "integrity": "sha512-p1mmqNKrCVlA4b6js3twZotAIdS1cLXh05oU9WmSAW3iDo1Vf9QO5+/9K1Vh3a9fPQt3nDJeD/OgAb7C4VBIsA==",
"dependencies": { "dependencies": {
"@sentry-internal/tracing": "7.102.1" "@sentry-internal/tracing": "7.104.0"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@sentry/tracing/node_modules/@sentry-internal/tracing": { "node_modules/@sentry/types": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.104.0.tgz",
"integrity": "sha512-RkFlFyAC0fQOvBbBqnq0CLmFW5m3JJz9pKbZd5vXPraWAlniKSb1bC/4DF9SlNx0FN1LWG+IU3ISdpzwwTeAGg==", "integrity": "sha512-5bs0xe0+GZR4QBm9Nrqw59o0sv3kBtCosrZDVxBru/dQbrfnB+/kVorvuM0rV3+coNITTKcKDegSZmK1d2uOGQ==",
"dependencies": {
"@sentry/core": "7.102.1",
"@sentry/types": "7.102.1",
"@sentry/utils": "7.102.1"
},
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/@sentry/tracing/node_modules/@sentry/core": { "node_modules/@sentry/utils": {
"version": "7.102.1", "version": "7.104.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.102.1.tgz", "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.104.0.tgz",
"integrity": "sha512-QjY+LSP3du3J/C8x/FfEbRxgZgsWd0jfTJ4P7s9f219I1csK4OeBMC3UA1HwEa0pY/9OF6H/egW2CjOcMM5Pdg==", "integrity": "sha512-ZVg+xZirI9DlOi0NegNVocswdh/8p6QkzlQzDQY2LP2CC6JQdmwi64o0S4rPH4YIHNKQJTpIjduoxeKgd1EO5g==",
"dependencies": { "dependencies": {
"@sentry/types": "7.102.1", "@sentry/types": "7.104.0"
"@sentry/utils": "7.102.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/tracing/node_modules/@sentry/types": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.102.1.tgz",
"integrity": "sha512-htKorf3t/D0XYtM7foTcmG+rM47rDP6XdbvCcX5gBCuCYlzpM1vqCt2rl3FLktZC6TaIpFRJw1TLfx6m+x5jdA==",
"engines": {
"node": ">=8"
}
},
"node_modules/@sentry/tracing/node_modules/@sentry/utils": {
"version": "7.102.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.102.1.tgz",
"integrity": "sha512-+8WcFjHVV/HROXSAwMuUzveElBFC43EiTG7SNEBNgOUeQzQVTmbUZXyTVgLrUmtoWqvnIxCacoLxtZo1o67kdg==",
"dependencies": {
"@sentry/types": "7.102.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -11230,25 +11049,25 @@
} }
}, },
"node_modules/firebase": { "node_modules/firebase": {
"version": "10.8.0", "version": "10.8.1",
"resolved": "https://registry.npmjs.org/firebase/-/firebase-10.8.0.tgz", "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.8.1.tgz",
"integrity": "sha512-UJpC24vw8JFuHEOQyArBGKTUd7+kohLISCzHyn0M/prP0KOTx2io1eyLliEid330QqnWI7FOlPxoU97qecCSfQ==", "integrity": "sha512-4B2jzhU/aumfKL446MG41/T5+t+9d9urf5XGrjC0HRQUm4Ya/amV48HBchnje69ExaJP5f2WxO9OX3wh9ee4wA==",
"dependencies": { "dependencies": {
"@firebase/analytics": "0.10.1", "@firebase/analytics": "0.10.1",
"@firebase/analytics-compat": "0.2.7", "@firebase/analytics-compat": "0.2.7",
"@firebase/app": "0.9.27", "@firebase/app": "0.9.28",
"@firebase/app-check": "0.8.2", "@firebase/app-check": "0.8.2",
"@firebase/app-check-compat": "0.3.9", "@firebase/app-check-compat": "0.3.9",
"@firebase/app-compat": "0.2.27", "@firebase/app-compat": "0.2.28",
"@firebase/app-types": "0.9.0", "@firebase/app-types": "0.9.0",
"@firebase/auth": "1.6.0", "@firebase/auth": "1.6.1",
"@firebase/auth-compat": "0.5.2", "@firebase/auth-compat": "0.5.3",
"@firebase/database": "1.0.3", "@firebase/database": "1.0.3",
"@firebase/database-compat": "1.0.3", "@firebase/database-compat": "1.0.3",
"@firebase/firestore": "4.4.2", "@firebase/firestore": "4.4.3",
"@firebase/firestore-compat": "0.3.25", "@firebase/firestore-compat": "0.3.26",
"@firebase/functions": "0.11.1", "@firebase/functions": "0.11.2",
"@firebase/functions-compat": "0.3.7", "@firebase/functions-compat": "0.3.8",
"@firebase/installations": "0.6.5", "@firebase/installations": "0.6.5",
"@firebase/installations-compat": "0.2.5", "@firebase/installations-compat": "0.2.5",
"@firebase/messaging": "0.12.6", "@firebase/messaging": "0.12.6",
@@ -11257,8 +11076,8 @@
"@firebase/performance-compat": "0.2.5", "@firebase/performance-compat": "0.2.5",
"@firebase/remote-config": "0.4.5", "@firebase/remote-config": "0.4.5",
"@firebase/remote-config-compat": "0.2.5", "@firebase/remote-config-compat": "0.2.5",
"@firebase/storage": "0.12.1", "@firebase/storage": "0.12.2",
"@firebase/storage-compat": "0.3.4", "@firebase/storage-compat": "0.3.5",
"@firebase/util": "1.9.4" "@firebase/util": "1.9.4"
} }
}, },
@@ -18174,16 +17993,16 @@
} }
}, },
"node_modules/query-string": { "node_modules/query-string": {
"version": "8.2.0", "version": "9.0.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.2.0.tgz", "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.0.0.tgz",
"integrity": "sha512-tUZIw8J0CawM5wyGBiDOAp7ObdRQh4uBor/fUR9ZjmbZVvw95OD9If4w3MQxr99rg0DJZ/9CIORcpEqU5hQG7g==", "integrity": "sha512-4EWwcRGsO2H+yzq6ddHcVqkCQ2EFUSfDMEjF8ryp8ReymyZhIuaFRGLomeOQLkrzacMHoyky2HW0Qe30UbzkKw==",
"dependencies": { "dependencies": {
"decode-uri-component": "^0.4.1", "decode-uri-component": "^0.4.1",
"filter-obj": "^5.1.0", "filter-obj": "^5.1.0",
"split-on-first": "^3.0.0" "split-on-first": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=14.16" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -19048,9 +18867,9 @@
} }
}, },
"node_modules/react-big-calendar": { "node_modules/react-big-calendar": {
"version": "1.10.3", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.10.3.tgz", "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.11.0.tgz",
"integrity": "sha512-LmIWlFfGUn8yt4RxcVkGNmjM3GcWynr1bfDwKrrz4KKj517+DH3OGmQzErURN6Zb0OB88HF4oH2dvDHpBQJgIw==", "integrity": "sha512-CabU9UCYIjbAew4GpDJa6ycrRINEEONbXEfO0YU7hHuHA2lhyMjnONY+GcpnJvhpOZC3wTtUa7krCriCm+iuKg==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.20.7", "@babel/runtime": "^7.20.7",
"clsx": "^1.2.1", "clsx": "^1.2.1",
@@ -19336,9 +19155,9 @@
} }
}, },
"node_modules/react-number-format": { "node_modules/react-number-format": {
"version": "5.3.1", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.1.tgz", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.3.3.tgz",
"integrity": "sha512-qpYcQLauIeEhCZUZY9jXZnnroOtdy3jYaS1zQ3M1Sr6r/KMOBEIGNIb7eKT19g2N1wbYgFgvDzs19hw5TrB8XQ==", "integrity": "sha512-maGHWmOvwYzyeRIpL0YC6drWqYaX6iFqjisdJXpZ+HzEtSEJsL6nqw4azTpF5Sm6SAvwUeAr7JY924Ebqq8EdA==",
"dependencies": { "dependencies": {
"prop-types": "^15.7.2" "prop-types": "^15.7.2"
}, },
@@ -19413,11 +19232,11 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.22.1", "version": "6.22.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.2.tgz",
"integrity": "sha512-0pdoRGwLtemnJqn1K0XHUbnKiX0S4X8CgvVVmHGOWmofESj31msHo/1YiqcJWK7Wxfq2a4uvvtS01KAQyWK/CQ==", "integrity": "sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.15.1" "@remix-run/router": "1.15.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -19427,12 +19246,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.22.1", "version": "6.22.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.2.tgz",
"integrity": "sha512-iwMyyyrbL7zkKY7MRjOVRy+TMnS/OPusaFVxM2P11x9dzSzGmLsebkCvYirGq0DWB9K9hOspHYYtDz33gE5Duw==", "integrity": "sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.15.1", "@remix-run/router": "1.15.2",
"react-router": "6.22.1" "react-router": "6.22.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -19662,9 +19481,9 @@
} }
}, },
"node_modules/recharts": { "node_modules/recharts": {
"version": "2.12.1", "version": "2.12.2",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.1.tgz", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.2.tgz",
"integrity": "sha512-35vUCEBPf+pM+iVgSgVTn86faKya5pc4JO6cYJL63qOK2zDEyzDn20Tdj+CDI/3z+VcpKyQ8ZBQ9OiQ+vuAbjg==", "integrity": "sha512-9bpxjXSF5g81YsKkTSlaX7mM4b6oYI1mIYck6YkUcWuL3tomADccI51/6thY4LmvhYuRTwpfrOvE80Zc3oBRfQ==",
"dependencies": { "dependencies": {
"clsx": "^2.0.0", "clsx": "^2.0.0",
"eventemitter3": "^4.0.1", "eventemitter3": "^4.0.1",
@@ -22605,9 +22424,9 @@
"integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw=="
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.26.5", "version": "5.28.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz",
"integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==",
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
}, },
@@ -24162,9 +23981,9 @@
} }
}, },
"node_modules/yauzl": { "node_modules/yauzl": {
"version": "3.1.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.0.tgz", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.1.tgz",
"integrity": "sha512-zbff6SaAPyewVextulqeBjJm+1ZhS69vSN7cRpqVD7jMNSE9oXEdQ1SGF+ydfB+gKE2a3GiWfXf/pnwVZ1/tOA==", "integrity": "sha512-MPxA7oN5cvGV0wzfkeHKF2/+Q4TkMpHSWGRy/96I4Cozljmx0ph91+Muxh6HegEtDC4GftJ8qYDE51vghFiEYA==",
"dependencies": { "dependencies": {
"buffer-crc32": "~0.2.3", "buffer-crc32": "~0.2.3",
"pend": "~1.2.0" "pend": "~1.2.0"

View File

@@ -13,8 +13,8 @@
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.1", "@reduxjs/toolkit": "^2.2.1",
"@sentry/cli": "^2.28.6", "@sentry/cli": "^2.28.6",
"@sentry/react": "^7.102.1", "@sentry/react": "^7.104.0",
"@sentry/tracing": "^7.102.1", "@sentry/tracing": "^7.104.0",
"@splitsoftware/splitio-react": "^1.11.0", "@splitsoftware/splitio-react": "^1.11.0",
"@tanem/react-nprogress": "^5.0.51", "@tanem/react-nprogress": "^5.0.51",
"antd": "^5.14.2", "antd": "^5.14.2",
@@ -29,7 +29,7 @@
"enquire-js": "^0.2.1", "enquire-js": "^0.2.1",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^10.8.0", "firebase": "^10.8.1",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"i18next": "^23.10.0", "i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.0.2", "i18next-browser-languagedetector": "^7.0.2",
@@ -42,11 +42,11 @@
"phone": "^3.1.42", "phone": "^3.1.42",
"preval.macro": "^5.0.0", "preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^8.2.0", "query-string": "^9.0.0",
"rc-queue-anim": "^2.0.0", "rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6", "rc-scroll-anim": "^2.7.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-big-calendar": "^1.10.3", "react-big-calendar": "^1.11.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^7.1.0", "react-cookie": "^7.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -58,15 +58,15 @@
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.8.1", "react-intersection-observer": "^9.8.1",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-number-format": "^5.1.4", "react-number-format": "^5.3.3",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-router-dom": "^6.22.1", "react-router-dom": "^6.22.2",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5", "react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"recharts": "^2.12.1", "recharts": "^2.12.2",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.3.0", "redux-saga": "^1.3.0",
@@ -84,7 +84,7 @@
"workbox-precaching": "^7.0.0", "workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0", "workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0", "workbox-strategies": "^7.0.0",
"yauzl": "^3.1.0" "yauzl": "^3.1.1"
}, },
"scripts": { "scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",

View File

@@ -5,10 +5,22 @@ import React, {useState} from "react";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {DELETE_BILL} from "../../graphql/bills.queries"; import {DELETE_BILL} from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import {insertAuditTrail} from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
export default function BillDeleteButton({bill, callback}) { const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const {t} = useTranslation(); const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL); const [deleteBill] = useMutation(DELETE_BILL);
const handleDelete = async () => { const handleDelete = async () => {
@@ -35,7 +47,11 @@ export default function BillDeleteButton({bill, callback}) {
}); });
if (!!!result.errors) { if (!!!result.errors) {
notification["success"]({message: t("bills.successes.deleted")}); notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
});
if (callback && typeof callback === "function") callback(bill.id); if (callback && typeof callback === "function") callback(bill.id);
} else { } else {

View File

@@ -50,17 +50,17 @@ export function BillsListTableComponent({
const Templates = TemplateList("bill"); const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : []; const bills = billsQuery.data ? billsQuery.data.bills : [];
const {refetch} = billsQuery; const { refetch } = billsQuery;
const recordActions = (record, showView = false) => ( const recordActions = (record, showView = false) => (
<Space wrap> <Space wrap>
{showView && ( {showView && (
<Button onClick={() => handleOnRowClick(record)}> <Button onClick={() => handleOnRowClick(record)}>
<EditFilled/> <EditFilled />
</Button> </Button>
)} )}
<BillDeleteButton bill={record}/> <BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent <BillDetailEditReturnComponent
data={{bills_by_pk: {...record, jobid: job.id}}} data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={ disabled={
record.is_credit_memo || record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid || record.vendorid === bodyshop.inhousevendorid ||

View File

@@ -1,22 +1,34 @@
import {SyncOutlined, WarningFilled} from "@ant-design/icons"; import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import {Button, Card, Dropdown, Input, Space, Table, Tooltip,} from "antd"; import {
Button,
Card,
Dropdown,
Input,
Space,
Table,
Tooltip,
} from "antd";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, {useState} from "react"; import React, { useState } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {Link} from "react-router-dom"; import { Link } from "react-router-dom";
import {DateTimeFormatter} from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
import {GenerateDocument} from "../../utils/RenderTemplate"; import { GenerateDocument } from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import {alphaSort} from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component"; import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({loading, courtesycars, refetch}) { export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: {text: ""},
}); });
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const {t} = useTranslation(); const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation();
const columns = [ const columns = [
{ {
@@ -42,6 +54,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
sorter: (a, b) => alphaSort(a.status, b.status), sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [ filters: [
{ {
text: t("courtesycars.status.in"), text: t("courtesycars.status.in"),
@@ -64,7 +77,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
sortOrder: sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order, state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => { render: (text, record) => {
const {nextservicedate, nextservicekm, mileage, insuranceexpires} = const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
record; record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
@@ -75,19 +88,23 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
const insuranceOver = const insuranceOver =
insuranceexpires && insuranceexpires &&
dayjs(insuranceexpires).endOf("day").isBefore(dayjs()); dayjs(insuranceexpires).endOf("day").isBefore(dayjs());
return ( return (
<Space> <Space>
{t(record.status)} {t(record.status)}
{(mileageOver || dueForService || insuranceOver) && ( {(mileageOver || dueForService || insuranceOver) && (
<Tooltip title={(mileageOver || dueForService) && insuranceOver <Tooltip
? t("contracts.labels.insuranceexpired") + title={
" / " + (mileageOver || dueForService) && insuranceOver
t("contracts.labels.cardueforservice") ? t("contracts.labels.insuranceexpired") +
: insuranceOver " / " +
? t("contracts.labels.insuranceexpired") t("contracts.labels.cardueforservice")
: t("contracts.labels.cardueforservice") : insuranceOver
}> ? t("contracts.labels.insuranceexpired")
<WarningFilled style={{color: "tomato"}}/> : t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} />
</Tooltip> </Tooltip>
)} )}
</Space> </Space>
@@ -99,6 +116,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
dataIndex: "readiness", dataIndex: "readiness",
key: "readiness", key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness), sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [ filters: [
{ {
text: t("courtesycars.readiness.ready"), text: t("courtesycars.readiness.ready"),
@@ -214,7 +232,8 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
]; ];
const handleTableChange = (pagination, filters, sorter) => { const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter}); setState({ ...state, sortedInfo: sorter });
setFilter(filters);
}; };
const tableData = searchText const tableData = searchText

View File

@@ -9,10 +9,12 @@ import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils"; import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries"; import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import {UPDATE_JOB} from "../../graphql/jobs.queries"; import {UPDATE_JOB} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { import {
selectBodyshop, selectBodyshop,
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -20,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -40,9 +47,10 @@ export function JobsCloseExportButton({
disabled, disabled,
setSelectedJobs, setSelectedJobs,
refetch, refetch,
insertAuditTrail,
}) { }) {
const history = useNavigate(); const history = useNavigate();
const {t} = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB); const [updateJob] = useMutation(UPDATE_JOB);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -181,6 +189,10 @@ export function JobsCloseExportButton({
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
});
updateJobCache( updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id) jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
); );
@@ -192,12 +204,20 @@ export function JobsCloseExportButton({
}); });
} }
} }
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
});
updateJobCache([ updateJobCache([
...new Set( ...new Set(
successfulTransactions.map( successfulTransactions.map(
@@ -227,4 +247,7 @@ export function JobsCloseExportButton({
); );
} }
export default connect(mapStateToProps, null)(JobsCloseExportButton); export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -237,6 +237,10 @@ export function JobsDetailHeaderActions({
message: JSON.stringify(result.errors), message: JSON.stringify(result.errors),
}), }),
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
});
return; return;
} }
if (e.key === "email") if (e.key === "email")
@@ -470,6 +474,24 @@ export function JobsDetailHeaderActions({
}); });
}; };
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
});
};
// Function to handle OK // Function to handle OK
const handleCancelScheduleOK = async () => { const handleCancelScheduleOK = async () => {
@@ -504,19 +526,6 @@ export function JobsDetailHeaderActions({
} }
}; };
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
};
const popOverContent = ( const popOverContent = (
<Card> <Card>
<div> <div>

View File

@@ -9,7 +9,12 @@ import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils"; import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries"; import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import {UPDATE_JOBS} from "../../graphql/jobs.queries"; import {UPDATE_JOBS} from "../../graphql/jobs.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient"; import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -17,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
}); });
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function updateJobCache(items) { function updateJobCache(items) {
client.cache.modify({ client.cache.modify({
id: "ROOT_QUERY", id: "ROOT_QUERY",
@@ -38,8 +48,9 @@ export function JobsExportAllButton({
loadingCallback, loadingCallback,
completedCallback, completedCallback,
refetch, refetch,
insertAuditTrail,
}) { }) {
const {t} = useTranslation(); const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS); const [updateJob] = useMutation(UPDATE_JOBS);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
@@ -168,47 +179,64 @@ export function JobsExportAllButton({
}, },
}); });
if (!!!jobUpdateResponse.errors) { if (!!!jobUpdateResponse.errors) {
notification.open({ notification.open({
type: "success", type: "success",
key: "jobsuccessexport", key: "jobsuccessexport",
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
updateJobCache( jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
jobUpdateResponse.data.update_jobs.returning.map( insertAuditTrail({
(job) => job.id jobid: job.id,
) operation: AuditTrailMapping.jobexported(),
); });
} else { });
notification["error"]({ updateJobCache(
message: t("jobs.errors.exporting", { jobUpdateResponse.data.update_jobs.returning.map(
error: JSON.stringify(jobUpdateResponse.error), (job) => job.id
}), )
}); );
} } else {
} notification["error"]({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) { message: t("jobs.errors.exporting", {
notification.open({ error: JSON.stringify(jobUpdateResponse.error),
type: "success", }),
key: "jobsuccessexport", });
message: t("jobs.successes.exported"), }
}); }
updateJobCache([ if (
...new Set( bodyshop.accountingconfig &&
successfulTransactions.map( bodyshop.accountingconfig.qbo &&
(st) => successfulTransactions.length > 0
st[ ) {
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo notification.open({
? "jobid" type: "success",
: "id" key: "jobsuccessexport",
] message: t("jobs.successes.exported"),
) });
), const successfulTransactionsSet = [
]); ...new Set(
} successfulTransactions.map(
} (st) =>
}) st[
); bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
];
if (successfulTransactionsSet.length > 0) {
insertAuditTrail({
jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(),
});
}
updateJobCache(successfulTransactionsSet);
}
}
})
);
if (!!completedCallback) completedCallback([]); if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false); if (!!loadingCallback) loadingCallback(false);
@@ -222,4 +250,7 @@ export function JobsExportAllButton({
); );
} }
export default connect(mapStateToProps, null)(JobsExportAllButton); export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsExportAllButton);

View File

@@ -59,8 +59,8 @@ const PaymentExpandedRowComponent = ({record, bodyshop}) => {
await insertPayment({ await insertPayment({
variables: { variables: {
paymentInput: { paymentInput: {
amount: -refund_response.data.amount, amount: -refund_response?.data?.amount,
transactionid: payment_response.response.receiptelements.transid, transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer, payer: record.payer,
type: "Refund", type: "Refund",
jobid: payment_response.jobid, jobid: payment_response.jobid,

View File

@@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils"; import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return ( return (
@@ -31,10 +31,9 @@ function FiltersSection({filters, form, bodyshop}) {
const {t} = useTranslation(); const {t} = useTranslation();
return ( return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} <Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
style={{marginTop: '10px'}}>
<Form.List name={["filters"]}> <Form.List name={["filters"]}>
{(fields, {add, remove, move}) => { {(fields, {add, remove}) => {
return ( return (
<div> <div>
{fields.map((field, index) => ( {fields.map((field, index) => (
@@ -72,7 +71,8 @@ function FiltersSection({filters, form, bodyshop}) {
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item <Form.Item
dependencies={[['filters', field.name, "field"]]}> dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
>
{ {
() => { () => {
const name = form.getFieldValue(['filters', field.name, "field"]); const name = form.getFieldValue(['filters', field.name, "field"]);
@@ -82,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) {
key={`${index}operator`} key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')} label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]} name={[field.name, "operator"]}
dependencies={[]}
rules={[ rules={[
{ {
required: true, required: true,
@@ -92,20 +91,32 @@ function FiltersSection({filters, form, bodyshop}) {
> >
<Select <Select
getPopupContainer={trigger => trigger.parentNode} getPopupContainer={trigger => trigger.parentNode}
options={getWhereOperatorsByType(type)}/> options={ getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], undefined);
}}
/>
</Form.Item> </Form.Item>
} }
} }
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item <Form.Item dependencies={[
dependencies={[['filters', field.name, "field"]]}> ['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{ {
() => { () => {
// Because it looks cleaner than inlining.
const name = form.getFieldValue(['filters', field.name, "field"]); const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type; const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector; const reflector = filters.find(f => f.name === name)?.reflector;
const operator = form.getFieldValue(['filters', field.name, "operator"]);
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
return <Form.Item return <Form.Item
key={`${index}value`} key={`${index}value`}
@@ -137,8 +148,22 @@ function FiltersSection({filters, form, bodyshop}) {
const reflections = reflector ? generateReflections(reflector) : []; const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]]; const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) { if (reflections.length > 0) {
// We have reflections and the operator type is array, so we will use a select box with multiple options
if (operatorType === "array") {
return (
<Select
disabled={!operator}
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return ( return (
<Select <Select
options={reflections} options={reflections}
@@ -150,16 +175,50 @@ function FiltersSection({filters, form, bodyshop}) {
); );
} }
// We have a type of number, so we will use a number input
if (type === "number") { if (type === "number") {
return ( return (
<InputNumber <InputNumber
disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/> onChange={(value) => form.setFieldValue(fieldPath, value)}/>
); );
} }
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>
);
}
// we have a type of boolean, so we will use a select box with a true or false option.
if (type === "boolean" || type === "bool") {
return (
<Select
disabled={!operator}
getPopupContainer={trigger => trigger.parentNode}
options={[
{
label: t('reportcenter.labels.advanced_filters_true'),
value: true
},
{
label: t('reportcenter.labels.advanced_filters_false'),
value: false
}
]}
onChange={(value) => form.setFieldValue(fieldPath, value)}
/>
);
}
return ( return (
<Input <Input
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/> disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
/>
); );
})() })()
} }
@@ -206,13 +265,12 @@ function FiltersSection({filters, form, bodyshop}) {
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
function SortersSection({sorters, form}) { function SortersSection({sorters}) {
const {t} = useTranslation(); const {t} = useTranslation();
return ( return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} <Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}> <Form.List name={["sorters"]}>
{(fields, {add, remove, move}) => { {(fields, {add, remove}) => {
return ( return (
<div> <div>
Sorters Sorters

View File

@@ -8,6 +8,21 @@ import {uniqBy} from "lodash";
*/ */
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj); const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
/**
* Generate options from array
* @param bodyshop
* @param path
* @returns {unknown[]}
*/
const generateOptionsFromArray = (bodyshop, path) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(options.map((value) => ({
label: value,
value: value,
})), 'value');
}
/** /**
* Valid internal reflections * Valid internal reflections
* Note: This is intended for future functionality * Note: This is intended for future functionality
@@ -46,15 +61,16 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
*/ */
const generateSpecialReflections = (bodyshop, finalPath) => { const generateSpecialReflections = (bodyshop, finalPath) => {
switch (finalPath) { switch (finalPath) {
// Special case because Referral Sources is an Array, not an Object.
case 'referral_source':
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
case 'class':
return generateOptionsFromArray(bodyshop, 'md_classes');
case 'cost_centers': case 'cost_centers':
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name'); return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
// Special case because Categories is an Array, not an Object. // Special case because Categories is an Array, not an Object.
case 'categories': case 'categories':
const catOptions = getValueFromPath(bodyshop, 'md_categories'); return generateOptionsFromArray(bodyshop, 'md_categories');
return uniqBy(catOptions.map((value) => ({
label: value,
value: value,
})), 'value');
case 'insurance_companies': case 'insurance_companies':
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
case 'employee_teams': case 'employee_teams':
@@ -118,4 +134,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
} }
}; };
export {generateInternalReflections,} export {generateInternalReflections}

View File

@@ -26,6 +26,8 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
return acc + value.bodyhrs; return acc + value.bodyhrs;
}, 0); }, 0);
const numJobs = entries.length;
return ( return (
<Card <Card
title={dayjs(date).format("D - ddd")} title={dayjs(date).format("D - ddd")}
@@ -34,17 +36,18 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
> >
<Statistic <Statistic
valueStyle={{color: dailyBodyTarget > bodyHrs ? "red" : "green"}} valueStyle={{color: dailyBodyTarget > bodyHrs ? "red" : "green"}}
label="B" label="Body"
value={bodyHrs.toFixed(1)} value={bodyHrs.toFixed(1)}
/> />
<Statistic <Statistic
valueStyle={{color: dailyPaintTarget > paintHrs ? "red" : "green"}} valueStyle={{color: dailyPaintTarget > paintHrs ? "red" : "green"}}
label="P" label="Refinish"
value={paintHrs.toFixed(1)} value={paintHrs.toFixed(1)}
/> />
<Divider style={{margin: 0}}/> <Divider style={{margin: 0}}/>
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)}/>
<Statistic value={(bodyHrs + paintHrs).toFixed(1)}/> <Divider style={{ margin: 0 }} />
<Statistic label="Jobs" value={numJobs} />
</Card> </Card>
); );
} }

View File

@@ -26,227 +26,260 @@ export function ScoreboardTargetsTable({bodyshop, scoreBoardlist}) {
const values = useMemo(() => { const values = useMemo(() => {
const dateHash = _.groupBy(scoreBoardlist, "date"); const dateHash = _.groupBy(scoreBoardlist, "date");
let ret = { let ret = {
todayBody: 0, todayBody: 0,
todayPaint: 0, todayPaint: 0,
weeklyPaint: 0, todayJobs: 0,
weeklyBody: 0, weeklyPaint: 0,
toDateBody: 0, weeklyJobs: 0,
toDatePaint: 0, weeklyBody: 0,
}; toDateBody: 0,
toDatePaint: 0,
toDateJobs: 0,
};
const today = dayjs(); const today = dayjs();
if (dateHash[today.format("YYYY-MM-DD")]) { if (dateHash[today.format("YYYY-MM-DD")]) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => { dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs; ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs; ret.todayPaint = ret.todayPaint + d.painthrs;
}); ret.todayJobs++;
} });
}
let StartOfWeek = dayjs().startOf("week"); let StartOfWeek = dayjs().startOf("week");
while (StartOfWeek.isSameOrBefore(today)) { while (StartOfWeek.isSameOrBefore(today)) {
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) { if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => { dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs; ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs; ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
}); ret.weeklyJobs++;
} });
StartOfWeek = StartOfWeek.add(1, "day"); }
} StartOfWeek = StartOfWeek.add(1, "day");
}
let startOfMonth = dayjs().startOf("month"); let startOfMonth = dayjs().startOf("month");
while (startOfMonth.isSameOrBefore(today)) { while (startOfMonth.isSameOrBefore(today)) {
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) { if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => { dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs; ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs; ret.toDatePaint = ret.toDatePaint + d.painthrs;
}); ret.toDateJobs++;
} });
startOfMonth = startOfMonth.add(1, "day"); }
} startOfMonth = startOfMonth.add(1, "day");
}
return ret; return ret;
}, [scoreBoardlist]); }, [scoreBoardlist]);
return ( return (
<Card <Card
title={t("scoreboard.labels.targets")} title={t("scoreboard.labels.targets")}
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist}/>} extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist} />}
> >
<Row gutter={rowGutter}> <Row gutter={rowGutter}>
<Col xs={24} sm={{offset: 0, span: 4}} lg={{span: 4}}> <Col xs={24} sm={{ offset: 0, span: 4 }} lg={{ span: 4 }}>
<Statistic <Statistic
title={t("scoreboard.labels.workingdays")} title={t("scoreboard.labels.workingdays")}
value={Util.CalculateWorkingDaysThisMonth()} value={Util.CalculateWorkingDaysThisMonth()}
prefix={<CalendarOutlined/>} prefix={<CalendarOutlined />}
/> />
</Col> </Col>
<Col xs={24} sm={{offset: 0, span: 20}} lg={{offset: 0, span: 20}}> <Col xs={24} sm={{ offset: 0, span: 20 }} lg={{ offset: 0, span: 20 }}>
<Row> <Row>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.dailytarget")} title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget} value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix="B" prefix={t("scoreboard.labels.bodyabbrev")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.dailyactual")} title={t("scoreboard.labels.dailyactual")}
value={values.todayBody.toFixed(1)} value={values.todayBody.toFixed(1)}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.weeklytarget")} title={t("scoreboard.labels.weeklytarget")}
value={Util.WeeklyTargetHrs( value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget, bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop bodyshop
)} )}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.weeklyactual")} title={t("scoreboard.labels.weeklyactual")}
value={values.weeklyBody.toFixed(1)} value={values.weeklyBody.toFixed(1)}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.monthlytarget")} title={t("scoreboard.labels.monthlytarget")}
value={Util.MonthlyTargetHrs( value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget, bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop bodyshop
)} )}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.asoftodaytarget")} title={t("scoreboard.labels.asoftodaytarget")}
value={Util.AsOfTodayTargetHrs( value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget, bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop bodyshop
)} )}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
title={t("scoreboard.labels.todateactual")} title={t("scoreboard.labels.todateactual")}
value={values.toDateBody.toFixed(1)} value={values.toDateBody.toFixed(1)}
/> />
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget} value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix="P" prefix={t("scoreboard.labels.refinishabbrev")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic value={values.todayPaint.toFixed(1)}/> <Statistic value={values.todayPaint.toFixed(1)} />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={Util.WeeklyTargetHrs( value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget, bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop bodyshop
)} )}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic value={values.weeklyPaint.toFixed(1)}/> <Statistic value={values.weeklyPaint.toFixed(1)} />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={Util.MonthlyTargetHrs( value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget, bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop bodyshop
)} )}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={Util.AsOfTodayTargetHrs( value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget, bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop bodyshop
)} )}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic value={values.toDatePaint.toFixed(1)}/> <Statistic value={values.toDatePaint.toFixed(1)} />
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Divider style={{margin: 5}}/> <Divider style={{ margin: 5 }} />
</Row> </Row>
<Row> <Row>
<Col {...statSpans}></Col> <Col {...statSpans}>
<Col {...statSpans}> <Statistic
<Statistic value={"\u00A0"}
value={(values.todayPaint + values.todayBody).toFixed(1)} prefix={t("scoreboard.labels.total")}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={( value={(values.todayPaint + values.todayBody).toFixed(1)}
Util.WeeklyTargetHrs( />
bodyshop.scoreboard_target.dailyBodyTarget, </Col>
bodyshop <Col {...statSpans}>
) + <Statistic
Util.WeeklyTargetHrs( value={(
bodyshop.scoreboard_target.dailyPaintTarget, Util.WeeklyTargetHrs(
bodyshop bodyshop.scoreboard_target.dailyBodyTarget,
) bodyshop
).toFixed(1)} ) +
/> Util.WeeklyTargetHrs(
</Col> bodyshop.scoreboard_target.dailyPaintTarget,
<Col {...statSpans}> bodyshop
<Statistic )
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)} ).toFixed(1)}
/> />
</Col> </Col>
<Col {...statSpans}> <Col {...statSpans}>
<Statistic <Statistic
value={( value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
Util.MonthlyTargetHrs( />
bodyshop.scoreboard_target.dailyBodyTarget, </Col>
bodyshop <Col {...statSpans}>
) + <Statistic
Util.MonthlyTargetHrs( value={(
bodyshop.scoreboard_target.dailyPaintTarget, Util.MonthlyTargetHrs(
bodyshop bodyshop.scoreboard_target.dailyBodyTarget,
) bodyshop
).toFixed(1)} ) +
/> Util.MonthlyTargetHrs(
</Col> bodyshop.scoreboard_target.dailyPaintTarget,
<Col {...statSpans}> bodyshop
<Statistic )
value={( ).toFixed(1)}
Util.AsOfTodayTargetHrs( />
bodyshop.scoreboard_target.dailyBodyTarget, </Col>
bodyshop <Col {...statSpans}>
) + <Statistic
Util.AsOfTodayTargetHrs( value={(
bodyshop.scoreboard_target.dailyPaintTarget, Util.AsOfTodayTargetHrs(
bodyshop bodyshop.scoreboard_target.dailyBodyTarget,
) bodyshop
).toFixed(1)} ) +
/> Util.AsOfTodayTargetHrs(
</Col> bodyshop.scoreboard_target.dailyPaintTarget,
<Col {...statSpans}> bodyshop
<Statistic )
value={(values.toDatePaint + values.toDateBody).toFixed(1)} ).toFixed(1)}
/> />
</Col> </Col>
</Row> <Col {...statSpans}>
</Col> <Statistic
</Row> value={(values.toDatePaint + values.toDateBody).toFixed(1)}
</Card> />
); </Col>
</Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.jobs")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.weeklyJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.toDateJobs} />
</Col>
</Row>
</Col>
</Row>
</Card>
);
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ScoreboardTargetsTable); )(ScoreboardTargetsTable);

View File

@@ -16,8 +16,13 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import {OwnerNameDisplayFunction} from "../../components/owner-name-display/owner-name-display.component"; import {OwnerNameDisplayFunction} from "../../components/owner-name-display/owner-name-display.component";
import {auth} from "../../firebase/firebase.utils"; import {auth} from "../../firebase/firebase.utils";
import {QUERY_JOB_EXPORT_DMS} from "../../graphql/jobs.queries"; import {QUERY_JOB_EXPORT_DMS} from "../../graphql/jobs.queries";
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions"; import {
insertAuditTrail,
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors"; import {selectBodyshop} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -26,6 +31,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer); export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
@@ -45,7 +52,12 @@ export const socket = SocketIO(
} }
); );
export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) { export function DmsContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
insertAuditTrail,
}) {
const {t} = useTranslation(); const {t} = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG"); const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate(); const history = useNavigate();
@@ -103,6 +115,10 @@ export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
notification.success({ notification.success({
message: t("jobs.successes.exported"), message: t("jobs.successes.exported"),
}); });
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
});
history("/manage/accounting/receivables"); history("/manage/accounting/receivables");
}); });

View File

@@ -281,12 +281,12 @@ export function* insertAuditTrailSaga({
yield client.mutate({ yield client.mutate({
mutation: INSERT_AUDIT_TRAIL, mutation: INSERT_AUDIT_TRAIL,
variables, variables,
update(cache, {data}) { update(cache, { data }) {
cache.modify({ cache.modify({
fields: { fields: {
audit_trail(existingAuditTrail, {readField}) { audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({ const newAuditTrail = cache.writeQuery({
data: data.insert_audit_trail_one, data: data,
query: INSERT_AUDIT_TRAIL, query: INSERT_AUDIT_TRAIL,
variables, variables,
}); });

View File

@@ -107,13 +107,14 @@
"alerttoggle": "Alert Toggle set to {{status}}", "alerttoggle": "Alert Toggle set to {{status}}",
"appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.", "appointmentcancel": "Appointment canceled. Lost Reason: {{lost_sale_reason}}.",
"appointmentinsert": "Appointment created. Appointment Date: {{start}}.", "appointmentinsert": "Appointment created. Appointment Date: {{start}}.",
"billposted": "Bill with invoice number {{invoice_number}} posted.", "billdeleted": "Bill with invoice number {{invoice_number}} deleted.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.", "billupdated": "Bill with invoice number {{invoice_number}} updated.",
"failedpayment": "Failed payment", "failedpayment": "Failed payment",
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}", "jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
"jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}",
"jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.",
"jobconverted": "Job converted and assigned number {{ro_number}}.", "jobconverted": "Job converted and assigned number {{ro_number}}.","jobexported": "Job has been exported.",
"jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.",
"jobimported": "Job imported.", "jobimported": "Job imported.",
"jobinproductionchange": "Job production status set to {{inproduction}}", "jobinproductionchange": "Job production status set to {{inproduction}}",
@@ -126,8 +127,9 @@
"jobspartsorder": "Parts order {{order_number}} added to Job.", "jobspartsorder": "Parts order {{order_number}} added to Job.",
"jobspartsreturn": "Parts return {{order_number}} added to Job.", "jobspartsreturn": "Parts return {{order_number}} added to Job.",
"jobstatuschange": "Job status changed to {{status}}.", "jobstatuschange": "Job status changed to {{status}}.",
"jobsupplement": "Job supplement imported." "jobsupplement": "Job supplement imported.",
} "jobsuspend": "Suspend Toggle set to {{status}}",
"jobvoid": "Job has been voided."}
}, },
"billlines": { "billlines": {
"actions": { "actions": {
@@ -2620,6 +2622,8 @@
"advanced_filters_sorters": "Sorters", "advanced_filters_sorters": "Sorters",
"advanced_filters_filter_field": "Field", "advanced_filters_filter_field": "Field",
"advanced_filters_sorter_field": "Field", "advanced_filters_sorter_field": "Field",
"advanced_filters_true": "True",
"advanced_filters_false": "False",
"advanced_filters_sorter_direction": "Direction", "advanced_filters_sorter_direction": "Direction",
"advanced_filters_filter_operator": "Operator", "advanced_filters_filter_operator": "Operator",
"advanced_filters_filter_value": "Value", "advanced_filters_filter_value": "Value",
@@ -2793,7 +2797,8 @@
"allemployeetimetickets": "All Employee Time Tickets", "allemployeetimetickets": "All Employee Time Tickets",
"asoftodaytarget": "As of Today", "asoftodaytarget": "As of Today",
"body": "Body", "body": "Body",
"bodycharttitle": "Body Targets vs Actual", "bodyabbrev": "B",
"bodycharttitle": "Body Targets vs Actual",
"calendarperiod": "Periods based on calendar weeks/months.", "calendarperiod": "Periods based on calendar weeks/months.",
"combinedcharttitle": "Combined Targets vs Actual", "combinedcharttitle": "Combined Targets vs Actual",
"dailyactual": "Actual (D)", "dailyactual": "Actual (D)",
@@ -2808,14 +2813,14 @@
"priorweek": "Prior Week", "priorweek": "Prior Week",
"productivestatistics": "Productive Hours Statistics", "productivestatistics": "Productive Hours Statistics",
"productivetimeticketsoverdate": "Productive Hours over Selected Dates", "productivetimeticketsoverdate": "Productive Hours over Selected Dates",
"refinish": "Refinish", "refinish": "Refinish","refinishabbrev": "R",
"refinishcharttitle": "Refinish Targets vs Actual", "refinishcharttitle": "Refinish Targets vs Actual",
"targets": "Targets", "targets": "Targets",
"thismonth": "This Month", "thismonth": "This Month",
"thisweek": "This Week", "thisweek": "This Week",
"timetickets": "Time Tickets", "timetickets": "Time Tickets",
"timeticketsemployee": "Time Tickets by Employee", "timeticketsemployee": "Time Tickets by Employee",
"todateactual": "Actual (MTD)", "todateactual": "Actual (MTD)","total": "Total",
"totalhrs": "Total Hours", "totalhrs": "Total Hours",
"totaloverperiod": "Total over Selected Dates", "totaloverperiod": "Total over Selected Dates",
"weeklyactual": "Actual (W)", "weeklyactual": "Actual (W)",

View File

@@ -107,14 +107,16 @@
"alerttoggle": "", "alerttoggle": "",
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"billposted": "", "billdeleted": "",
"billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
"jobassignmentchange": "", "jobassignmentchange": "",
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobfieldchanged": "", "jobexported": "",
"jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
"jobinvoiced": "", "jobinvoiced": "",
@@ -126,8 +128,9 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "" "jobsupplement": "",
} "jobsuspend": "",
"jobvoid": ""}
}, },
"billlines": { "billlines": {
"actions": { "actions": {
@@ -2620,6 +2623,8 @@
"advanced_filters_sorters": "", "advanced_filters_sorters": "",
"advanced_filters_filter_field": "", "advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "", "advanced_filters_sorter_field": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "", "advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "", "advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "", "advanced_filters_filter_value": "",
@@ -2793,7 +2798,8 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodycharttitle": "", "bodyabbrev": "",
"bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
"dailyactual": "", "dailyactual": "",
@@ -2809,13 +2815,14 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishcharttitle": "", "refinishabbrev": "",
"refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
"thisweek": "", "thisweek": "",
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "","total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -107,14 +107,16 @@
"alerttoggle": "", "alerttoggle": "",
"appointmentcancel": "", "appointmentcancel": "",
"appointmentinsert": "", "appointmentinsert": "",
"billposted": "", "billdeleted": "",
"billposted": "",
"billupdated": "", "billupdated": "",
"failedpayment": "", "failedpayment": "",
"jobassignmentchange": "", "jobassignmentchange": "",
"jobassignmentremoved": "", "jobassignmentremoved": "",
"jobchecklist": "", "jobchecklist": "",
"jobconverted": "", "jobconverted": "",
"jobfieldchanged": "", "jobexported": "",
"jobfieldchanged": "",
"jobimported": "", "jobimported": "",
"jobinproductionchange": "", "jobinproductionchange": "",
"jobinvoiced": "", "jobinvoiced": "",
@@ -126,8 +128,9 @@
"jobspartsorder": "", "jobspartsorder": "",
"jobspartsreturn": "", "jobspartsreturn": "",
"jobstatuschange": "", "jobstatuschange": "",
"jobsupplement": "" "jobsupplement": "",
} "jobsuspend": "",
"jobvoid": ""}
}, },
"billlines": { "billlines": {
"actions": { "actions": {
@@ -2620,6 +2623,8 @@
"advanced_filters_sorters": "", "advanced_filters_sorters": "",
"advanced_filters_filter_field": "", "advanced_filters_filter_field": "",
"advanced_filters_sorter_field": "", "advanced_filters_sorter_field": "",
"advanced_filters_true": "",
"advanced_filters_false": "",
"advanced_filters_sorter_direction": "", "advanced_filters_sorter_direction": "",
"advanced_filters_filter_operator": "", "advanced_filters_filter_operator": "",
"advanced_filters_filter_value": "", "advanced_filters_filter_value": "",
@@ -2793,7 +2798,8 @@
"allemployeetimetickets": "", "allemployeetimetickets": "",
"asoftodaytarget": "", "asoftodaytarget": "",
"body": "", "body": "",
"bodycharttitle": "", "bodyabbrev": "",
"bodycharttitle": "",
"calendarperiod": "", "calendarperiod": "",
"combinedcharttitle": "", "combinedcharttitle": "",
"dailyactual": "", "dailyactual": "",
@@ -2809,13 +2815,14 @@
"productivestatistics": "", "productivestatistics": "",
"productivetimeticketsoverdate": "", "productivetimeticketsoverdate": "",
"refinish": "", "refinish": "",
"refinishcharttitle": "", "refinishabbrev": "",
"refinishcharttitle": "",
"targets": "", "targets": "",
"thismonth": "", "thismonth": "",
"thisweek": "", "thisweek": "",
"timetickets": "", "timetickets": "",
"timeticketsemployee": "", "timeticketsemployee": "",
"todateactual": "", "todateactual": "","total": "",
"totalhrs": "", "totalhrs": "",
"totaloverperiod": "", "totaloverperiod": "",
"weeklyactual": "", "weeklyactual": "",

View File

@@ -20,6 +20,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.appointmentcancel", {lost_sale_reason}), i18n.t("audit_trail.messages.appointmentcancel", {lost_sale_reason}),
appointmentinsert: (start) => appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", {start}), i18n.t("audit_trail.messages.appointmentinsert", {start}),
billdeleted: (invoice_number) =>
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billposted: (invoice_number) => billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", {invoice_number}), i18n.t("audit_trail.messages.billposted", {invoice_number}),
billupdated: (invoice_number) => billupdated: (invoice_number) =>
@@ -33,6 +35,7 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobchecklist", {type, inproduction, status}), i18n.t("audit_trail.messages.jobchecklist", {type, inproduction, status}),
jobconverted: (ro_number) => jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", {ro_number}), i18n.t("audit_trail.messages.jobconverted", {ro_number}),
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
jobfieldchange: (field, value) => jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", {field, value}), i18n.t("audit_trail.messages.jobfieldchanged", {field, value}),
jobimported: () => i18n.t("audit_trail.messages.jobimported"), jobimported: () => i18n.t("audit_trail.messages.jobimported"),
@@ -51,6 +54,8 @@ const AuditTrailMapping = {
jobstatuschange: (status) => jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", {status}), i18n.t("audit_trail.messages.jobstatuschange", {status}),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
}; };
export default AuditTrailMapping; export default AuditTrailMapping;

View File

@@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient"; import client from "./GraphQLClient";
import {gql} from "@apollo/client"; import {gql} from "@apollo/client";
/* eslint-disable no-loop-func */
/**
* The available operators for filtering (string)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const STRING_OPERATORS = [ const STRING_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"}, {value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"}, {value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"}, {value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain case-insensitive"} {value: "_nilike", label: "does not contain case-insensitive"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
]; ];
/**
* The available operators for filtering (dates)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const DATE_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for filtering (booleans)
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const BOOLEAN_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
];
/**
* The available operators for filtering (numbers)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const NUMBER_OPERATORS = [ const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"}, {value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"}, {value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"}, {value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"} {value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
]; ];
/**
* The available operators for sorting
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const ORDER_BY_OPERATORS = [ const ORDER_BY_OPERATORS = [
{value: "asc", label: "ascending"}, {value: "asc", label: "ascending"},
{value: "desc", label: "descending"} {value: "desc", label: "descending"}
@@ -31,7 +75,6 @@ export function getOrderOperatorsByType() {
return ORDER_BY_OPERATORS; return ORDER_BY_OPERATORS;
} }
/** /**
* Get the available operators for filtering * Get the available operators for filtering
* @param type * @param type
@@ -40,13 +83,14 @@ export function getOrderOperatorsByType() {
export function getWhereOperatorsByType(type = 'string') { export function getWhereOperatorsByType(type = 'string') {
const operators = { const operators = {
string: STRING_OPERATORS, string: STRING_OPERATORS,
number: NUMBER_OPERATORS number: NUMBER_OPERATORS,
boolean: BOOLEAN_OPERATORS,
bool: BOOLEAN_OPERATORS,
date: DATE_OPERATORS
}; };
return operators[type]; return operators[type];
} }
/* eslint-disable no-loop-func */
/** /**
* Parse a GraphQL query into an AST * Parse a GraphQL query into an AST
* @param query * @param query
@@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
// Parse the query and apply the filters and sorters // Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute); const ast = parseQuery(templateQueryToExecute);
let filterFields = [];
if (templateObject?.filters && templateObject?.filters?.length) { if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields); applyFilters(ast, templateObject.filters);
wrapFiltersInAnd(ast, filterFields);
} }
if (templateObject?.sorters && templateObject?.sorters?.length) { if (templateObject?.sorters && templateObject?.sorters?.length) {
@@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
return {contextData, useShopSpecificTemplate}; return {contextData, useShopSpecificTemplate};
} }
/** /**
* Apply sorters to the AST * Apply sorters to the AST
* @param ast * @param ast
@@ -170,10 +211,59 @@ export function applySorters(ast, sorters) {
}); });
} }
/**
* Apply Top Level Sub to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyTopLevelSub(node, fieldPath, filterField) {
// Find or create the where argument for the top-level subfield
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSubSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSubSelection) {
topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
topLevelSubSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/** /**
* Apply filters to the AST * Apply filters to the AST
* @param ast * @param ast
* @param filters * @param filters
* @returns {ASTNode}
*/ */
export function applyFilters(ast, filters) { export function applyFilters(ast, filters) {
return visit(ast, { return visit(ast, {
@@ -182,192 +272,197 @@ export function applyFilters(ast, filters) {
filters.forEach(filter => { filters.forEach(filter => {
const fieldPath = filter.field.split('.'); const fieldPath = filter.field.split('.');
let topLevel = false; let topLevel = false;
let topLevelSub = false;
// Determine if the filter should be applied at the top level // Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { if (fieldPath.length === 2) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true; topLevel = true;
} }
if (topLevel) { if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
// Construct the filter for a top-level application fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
const targetFieldName = fieldPath[fieldPath.length - 1]; topLevelSub = true;
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetFieldName},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(nestedFilter);
} else { // Directly under the top level
whereArg.value.fields.push(nestedFilter);
}
} else {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
let whereArgFound = false;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (whereArg) {
whereArgFound = true;
} else {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetField},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
} }
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
let filterValue = createFilterValue(filter);
let filterField = createFilterField(targetFieldName, filter, filterValue);
if (topLevel) {
applyTopLevelFilter(node, fieldPath, filterField);
} else if (topLevelSub) {
applyTopLevelSub(node, fieldPath, filterField);
} else {
applyNestedFilter(node, fieldPath, filterField);
}
}); });
} }
} }
}); });
} }
/**
* Create a filter value based on the filter
* @param filter
* @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
*/
function createFilterValue(filter) {
if (Array.isArray(filter.value)) {
// If it's an array, create a list value with the array items
return {
kind: Kind.LIST,
values: filter.value.map(item => ({
kind: getGraphQLKind(item),
value: item,
})),
};
} else {
// If it's not an array, use the existing logic
return {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
}
}
/**
* Create a filter field based on the target field and filter
* @param targetFieldName
* @param filter
* @param filterValue
* @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
*/
function createFilterField(targetFieldName, filter, filterValue) {
return {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetFieldName},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
}
/**
* Apply a top-level filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyTopLevelFilter(node, fieldPath, filterField) {
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/**
* Apply a nested filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyNestedFilter(node, fieldPath, filterField) {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Add the filter field to the 'where' clause of the current selection
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
/** /**
* Get the GraphQL kind for a value * Get the GraphQL kind for a value
* @param value * @param value
* @returns {Kind|Kind.INT} * @returns {Kind|Kind.INT}
*/ */
function getGraphQLKind(value) { function getGraphQLKind(value) {
if (typeof value === 'number') { if (Array.isArray(value)) {
return Kind.LIST;
} else if (typeof value === 'number') {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT; return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') { } else if (typeof value === 'boolean') {
return Kind.BOOLEAN; return Kind.BOOLEAN;
} else if (typeof value === 'string') { } else if (typeof value === 'string') {
return Kind.STRING; return Kind.STRING;
} else if (value instanceof Date) {
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
} }
// Extend with more types as needed
}
/**
* Wrap filters in an 'and' object
* @param ast
* @param filterFields
*/
export function wrapFiltersInAnd(ast, filterFields) {
visit(ast, {
OperationDefinition: {
enter(node) {
node.selectionSet.selections.forEach((selection) => {
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
if (filterFields.length > 1) {
const andFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: '_and'},
value: {kind: Kind.LIST, values: filterFields}
};
whereArg.value.fields.push(andFilter);
} else if (filterFields.length === 1) {
whereArg.value.fields.push(filterFields[0].fields[0]);
}
});
}
}
});
} }
/* eslint-enable no-loop-func */ /* eslint-enable no-loop-func */

1878
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,49 +18,51 @@
"start": "node server.js" "start": "node server.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.515.0", "@aws-sdk/client-secrets-manager": "^3.525.0",
"@aws-sdk/client-ses": "^3.515.0", "@aws-sdk/client-ses": "^3.525.0",
"@aws-sdk/credential-provider-node": "^3.515.0", "@aws-sdk/credential-provider-node": "^3.525.0",
"@azure/storage-blob": "^12.17.0",
"@opensearch-project/opensearch": "^2.5.0", "@opensearch-project/opensearch": "^2.5.0",
"aws4": "^1.12.0", "aws4": "^1.12.0",
"axios": "^1.6.5", "axios": "^1.6.5",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"cloudinary": "^2.0.1", "cloudinary": "^2.0.2",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "2.8.5", "cors": "2.8.5",
"csrf": "^3.1.0", "csrf": "^3.1.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.18.2", "express": "^4.18.3",
"firebase-admin": "^12.0.0", "firebase-admin": "^12.0.0",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"graylog2": "^0.2.1", "graylog2": "^0.2.1",
"inline-css": "^4.0.2", "inline-css": "^4.0.2",
"intuit-oauth": "^4.0.0", "intuit-oauth": "^4.0.0",
"json-2-csv": "^5.2.0", "json-2-csv": "^5.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.45", "moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.5", "node-mailjet": "^6.0.5",
"node-persist": "^4.0.1", "node-persist": "^4.0.1",
"node-quickbooks": "^2.0.43", "node-quickbooks": "^2.0.44",
"nodemailer": "^6.9.10", "nodemailer": "^6.9.11",
"phone": "^3.1.42", "phone": "^3.1.42",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"soap": "^1.0.0", "soap": "^1.0.0",
"socket.io": "^4.7.4", "socket.io": "^4.7.4",
"ssh2-sftp-client": "^10.0.3", "ssh2-sftp-client": "^10.0.3",
"stripe": "^14.18.0", "stripe": "^14.19.0",
"twilio": "^4.22.0", "twilio": "^4.23.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"source-map-explorer": "^2.5.2" "source-map-explorer": "^2.5.2"
} }