diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e047be49..5d6f8f506 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,31 +42,22 @@ jobs: app-build: docker: - image: cimg/node:16.15.0 - + resource_class: large working_directory: ~/repo/client steps: - checkout: path: ~/repo - - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-{{ checksum "yarn.lock" }} - run: name: Install Dependencies - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn + command: npm i - - run: yarn run build + - run: npm run build - aws-s3/sync: from: build to: "s3://imex-online-production/" + arguments: "--exclude '*.map'" - jira/notify test-hasura-migrate: @@ -92,31 +83,22 @@ jobs: test-app-build: docker: - image: cimg/node:16.15.0 - + resource_class: large working_directory: ~/repo/client steps: - checkout: path: ~/repo - - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-{{ checksum "yarn.lock" }} - run: name: Install Dependencies - command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn + command: npm i - - run: yarn run build:test + - run: npm run build:test - aws-s3/sync: from: build to: "s3://imex-online-test/" + arguments: "--exclude '*.map'" - jira/notify admin-app-build: @@ -177,4 +159,4 @@ workflows: #- admin-app-build: #filters: #branches: - #only: master \ No newline at end of file + #only: master diff --git a/_reference/reportFiltersAndSorters.md b/_reference/reportFiltersAndSorters.md new file mode 100644 index 000000000..1bd950794 --- /dev/null +++ b/_reference/reportFiltersAndSorters.md @@ -0,0 +1,167 @@ +# Filters and Sorters + +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. + +# 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 + +## High level Schema Overview + +```javascript +const schema = { + "filters": [ + { + "name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query + "translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI + "label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation + "type": "number" // Type of field, can be number or string currently + }, + // ... more filters + ], + "sorters": [ + { + "name": "jobs.joblines.mod_lb_hrs", // Name and path of the field in the graphQL query + "translation": "jobs.joblines.mod_lb_hrs_1", // Translation key for the label used in the GUI + "label": "mod_lb_hrs_1", // Label used in the case the GUI does not contain a translation + "type": "number" // Type of field, can be number or string currently + }, + // ... more sorters + ], + "dates": { + // This is not yet implemented and will be added in a future release + } +} +``` + +## Filters + +Filters effect the where clause of the graphQL query. They are used to filter the data returned from the server. +A note on special notation used in the `name` field. + +## Reflection +Filters can make use of reflection to pre-fill select boxes, the following is an example of that in the filters file. + +``` + { + "name": "jobs.status", + "translation": "jobs.fields.status", + "label": "Status", + "type": "string", + "reflector": { + "type": "internal", + "name": "special.job_statuses" + } + }, +``` + +in this example, a reflector with the type 'internal' (all types at the moment require this, and it is used for future functionality), with a name of `special.job_statuses` + +The following cases are available + +- `special.job_statuses` - This will reflect the statuses of the jobs table `bodyshop.md_ro_statuses.statuses'` +- `special.cost_centers` - This will reflect the cost centers `bodyshop.md_responsibility_centers.costs` +- `special.categories` - This will reflect the categories `bodyshop.md_categories` +- `special.insurance_companies` - This will reflect the insurance companies `bodyshop.md_ins_cos`' +- `special.employee_teams` - This will reflect the employee teams `bodyshop.employee_teams` +- `special.employees` - This will reflect the employees `bodyshop.employees` +- `special.first_names` - This will reflect the first names `bodyshop.employees` +- `special.last_names` - This will reflect the last names `bodyshop.employees` +- +### Path without brackets, multi level + +`"name": "jobs.joblines.mod_lb_hrs",` +This will produce a where clause at the `joblines` level of the graphQL query, + +```graphql +query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) { + jobs( + where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}} + ) { + joblines( + order_by: {line_no: asc} + where: {removed: {_eq: false}, mod_lb_hrs: {_lt: 3}} + ) { + line_no + mod_lbr_ty + mod_lb_hrs + convertedtolbr + convertedtolbr_data + } + ownr_co_nm + ownr_fn + ownr_ln + plate_no + ro_number + status + v_make_desc + v_model_desc + v_model_yr + v_vin + v_color + } +} +``` + +### Path with brackets,top level +`"name": "[jobs].joblines.mod_lb_hrs",` +This will produce a where clause at the `jobs` level of the graphQL query. + +```graphql +query gendoc_hours_sold_detail_open($starttz: timestamptz!, $endtz: timestamptz!) { + jobs( + where: {date_invoiced: {_is_null: true}, date_open: {_gte: $starttz, _lte: $endtz}, ro_number: {_is_null: false}, voided: {_eq: false}, joblines: {mod_lb_hrs: {_gt: 4}}} + ) { + joblines( + order_by: {line_no: asc} + where: {removed: {_eq: false}} + ) { + line_no + mod_lbr_ty + mod_lb_hrs + convertedtolbr + convertedtolbr_data + } + ownr_co_nm + ownr_fn + ownr_ln + plate_no + ro_number + status + v_make_desc + v_model_desc + v_model_yr + v_vin + v_color + } +} +``` + +## Known Caveats +- 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. +- The `dates` object is not yet implemented and will be added in a future release. +- 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. +- 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. +- Do not add the ability to filter on things like FK constraints, must like the above example. + +## Sorters +- Sorters follow the same schema as filters, however, they do not do square bracket wrapping to indicate level hoisting, a filter added on `job.md_status` would be added at the top level, and a filter added on `jobs.joblines.mod_lb_hrs` would be added at the `joblines` level. +- Most of the reports currently do sorting on a template level, this will need to change to actually see the results using the 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. +- 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`. + +```json +{ + "name": "jobs.joblines.mod_lb_hrs", + "translation": "jobs.joblines.mod_lb_hrs_1", + "label": "mod_lb_hrs_1", + "type": "number", + "default": { + "order": 1, + "direction": "asc" + } +} +``` \ No newline at end of file diff --git a/client/.env.production b/client/.env.production index a4cdecee5..cdf721dcb 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,4 +1,4 @@ -GENERATE_SOURCEMAP=false +GENERATE_SOURCEMAP=true REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql REACT_APP_GA_CODE=231103507 diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 000000000..387b4257c --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,3 @@ + +# Sentry Config File +.sentryclirc diff --git a/client/craco.config.js b/client/craco.config.js index ac2174e3f..66cddad73 100644 --- a/client/craco.config.js +++ b/client/craco.config.js @@ -1,25 +1,25 @@ // craco.config.js const TerserPlugin = require("terser-webpack-plugin"); const CracoLessPlugin = require("craco-less"); -const SentryWebpackPlugin = require("@sentry/webpack-plugin"); +//const SentryWebpackPlugin = require("@sentry/webpack-plugin"); module.exports = { plugins: [ - { - plugin: SentryWebpackPlugin, - options: { - // sentry-cli configuration - authToken: - "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260", - org: "snapt-software", - project: "imexonline", - release: process.env.REACT_APP_GIT_SHA, + // { + // plugin: SentryWebpackPlugin, + // options: { + // // sentry-cli configuration + // authToken: + // "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260", + // org: "snapt-software", + // project: "imexonline", + // release: process.env.REACT_APP_GIT_SHA, - // webpack-specific configuration - include: ".", - ignore: ["node_modules", "webpack.config.js"], - }, - }, + // // webpack-specific configuration + // include: ".", + // ignore: ["node_modules", "webpack.config.js"], + // }, + // }, { plugin: CracoLessPlugin, options: { diff --git a/client/package-lock.json b/client/package-lock.json index 0409aecde..25a526f60 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,12 +13,14 @@ "@craco/craco": "^7.0.0", "@fingerprintjs/fingerprintjs": "^3.4.2", "@jsreport/browser-client": "^3.1.0", - "@sentry/react": "^7.40.0", + "@sentry/cli": "^2.27.0", + "@sentry/react": "^7.99.0", "@sentry/tracing": "^7.40.0", "@splitsoftware/splitio-react": "^1.8.1", "@tanem/react-nprogress": "^5.0.8", "antd": "^4.24.8", "apollo-link-logger": "^2.0.1", + "apollo-link-sentry": "^3.3.0", "axios": "^1.3.4", "craco-less": "^2.0.0", "dinero.js": "^1.9.1", @@ -4214,40 +4216,195 @@ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", "license": "MIT" }, - "node_modules/@sentry/browser": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.40.0.tgz", - "integrity": "sha512-07rZ+cTcpmYB1r84/oZtmSPJJvLCxW8yIh/5s4MdKRyZpqIDKhOz6cCS/4j+l1V+MeLcNLZBjFtNdKA2eocTpg==", - "license": "MIT", + "node_modules/@sentry-internal/feedback": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-7.99.0.tgz", + "integrity": "sha512-exIO1o+bE0MW4z30FxC0cYzJ4ZHSMlDPMHCBDPzU+MWGQc/fb8s58QUrx5Dnm6HTh9G3H+YlroCxIo9u0GSwGQ==", "dependencies": { - "@sentry/core": "7.40.0", - "@sentry/replay": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", - "tslib": "^1.9.3" + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" }, "engines": { "node": ">=8" } }, - "node_modules/@sentry/browser/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@sentry-internal/feedback/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-7.99.0.tgz", + "integrity": "sha512-PoIkfusToDq0snfl2M6HJx/1KJYtXxYhQplrn11kYadO04SdG0XGXf4h7wBTMEQ7LDEAtQyvsOu4nEQtTO3YjQ==", + "dependencies": { + "@sentry/core": "7.99.0", + "@sentry/replay": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.99.0.tgz", + "integrity": "sha512-z3JQhHjoM1KdM20qrHwRClKJrNLr2CcKtCluq7xevLtXHJWNAQQbafnWD+Aoj85EWXBzKt9yJMv2ltcXJ+at+w==", + "dependencies": { + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry-internal/tracing/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-7.99.0.tgz", + "integrity": "sha512-bgfoUv3wkwwLgN5YUOe0ibB3y268ZCnamZh6nLFqnY/UBKC1+FXWFdvzVON/XKUm62LF8wlpCybOf08ebNj2yg==", + "dependencies": { + "@sentry-internal/feedback": "7.99.0", + "@sentry-internal/replay-canvas": "7.99.0", + "@sentry-internal/tracing": "7.99.0", + "@sentry/core": "7.99.0", + "@sentry/replay": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@sentry/cli": { - "version": "1.74.6", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.74.6.tgz", - "integrity": "sha512-pJ7JJgozyjKZSTjOGi86chIngZMLUlYt2HOog+OJn+WGvqEkVymu8m462j1DiXAnex9NspB4zLLNuZ/R6rTQHg==", - "dev": true, + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.27.0.tgz", + "integrity": "sha512-pc0opd71W8lGhYvmB1keQtJkarxzCS9f9ErKYv6TfXOOX6drvwkyA6vD/6xEnpzyvqGAuGRU4T4sEeLD3irwUQ==", "hasInstallScript": true, - "license": "BSD-3-Clause", "dependencies": { "https-proxy-agent": "^5.0.0", - "mkdirp": "^0.5.5", "node-fetch": "^2.6.7", - "npmlog": "^4.1.2", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" @@ -4256,7 +4413,124 @@ "sentry-cli": "bin/sentry-cli" }, "engines": { - "node": ">= 8" + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.27.0", + "@sentry/cli-linux-arm": "2.27.0", + "@sentry/cli-linux-arm64": "2.27.0", + "@sentry/cli-linux-i686": "2.27.0", + "@sentry/cli-linux-x64": "2.27.0", + "@sentry/cli-win32-i686": "2.27.0", + "@sentry/cli-win32-x64": "2.27.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.27.0.tgz", + "integrity": "sha512-/DOZlN5rK19g7YP2OaVNauQhUrRfJ88RDr6qURFiqdxYHDc3isPFGHZJmeZBTwOnDDepyZb4XLaOyfwvAOxHig==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.27.0.tgz", + "integrity": "sha512-JmMQ9zgFhkZUEN5WIYuJisu4Jif/ThRHDjbsbXBRbUkkgRn88hgUfg299djMvlZZxjpl3K9AEua+1TIUeQd0Sg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.27.0.tgz", + "integrity": "sha512-f+zuB9XGfB8pNamNgSDhqsavuLuzi6saZxbr3uQf30bA5AESI5hspOd1zPcidOORCVZxiPzQe3+T7avBI1XLuw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.27.0.tgz", + "integrity": "sha512-/4eyz7jnYp20mZqNtpvCEBkxFW0nEjEZRo2BiASQ5/7K8CmoJRe1vhpDA0WOfzi1zTFIfpdE1/RZm2CjHS6DHQ==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.27.0.tgz", + "integrity": "sha512-ptu7wXecnYssihzHlxEOaqbFHWmNEfbepBKGXTdWK2kC+D51+7yHsR9xRdThwVID1bisFgjAveKmBQjmKuXjHQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.27.0.tgz", + "integrity": "sha512-Db4/xmdE5qV4Aq7Yc8vRw22Y46JJdGMdsMsl5jIf0GVSQPgO23O/2uTiDGpPOdeq91K9EtvpH1zQfDLIfLMaXw==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.27.0.tgz", + "integrity": "sha512-q7y/BH4iGfs0TD5PXh2Q8oqnTbOIufoT1NWJcKqvZcOiqCLK3PNUiq7xUeX1PMTrFYAh3Bm6EekOnMavqvbGmg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, "node_modules/@sentry/core": { @@ -4280,16 +4554,15 @@ "license": "0BSD" }, "node_modules/@sentry/react": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.40.0.tgz", - "integrity": "sha512-7yYagpOCdsXnVTtLL8Y7wAf2xXgsk2ncuju3O/G4kEckkLewZWmQeoknOSGFlAgVdGNhTaXc2WGzgOiBMOkhug==", - "license": "MIT", + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-7.99.0.tgz", + "integrity": "sha512-RtHwgzMHJhzJfSQpVG0SDPQYMTGDX3Q37/YWI59S4ALMbSW4/F6n/eQAvGVYZKbh2UCSqgFuRWaXOYkSZT17wA==", "dependencies": { - "@sentry/browser": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", - "hoist-non-react-statics": "^3.3.2", - "tslib": "^1.9.3" + "@sentry/browser": "7.99.0", + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0", + "hoist-non-react-statics": "^3.3.2" }, "engines": { "node": ">=8" @@ -4298,26 +4571,82 @@ "react": "15.x || 16.x || 17.x || 18.x" } }, - "node_modules/@sentry/react/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" + "node_modules/@sentry/react/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/react/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/react/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@sentry/replay": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.40.0.tgz", - "integrity": "sha512-Y9Kvo9jKouUdrHQhHVv5SmWZClF5o7BFI6oVpLlv4zXORPQlyoZONM/9sxiMvvH73alDSpxzCoxyhlypAOH4ww==", - "license": "MIT", + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-7.99.0.tgz", + "integrity": "sha512-gyN/I2WpQrLAZDT+rScB/0jnFL2knEVBo8U8/OVt8gNP20Pq8T/rDZKO/TG0cBfvULDUbJj2P4CJryn2p/O2rA==", "dependencies": { - "@sentry/core": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0" + "@sentry-internal/tracing": "7.99.0", + "@sentry/core": "7.99.0", + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" }, "engines": { "node": ">=12" } }, + "node_modules/@sentry/replay/node_modules/@sentry/core": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.99.0.tgz", + "integrity": "sha512-vOAtzcAXEUtS/oW7wi3wMkZ3hsb5Ch96gKyrrj/mXdOp2zrcwdNV6N9/pawq2E9P/7Pw8AXw4CeDZztZrjQLuA==", + "dependencies": { + "@sentry/types": "7.99.0", + "@sentry/utils": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/types": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.99.0.tgz", + "integrity": "sha512-94qwOw4w40sAs5mCmzcGyj8ZUu/KhnWnuMZARRq96k+SjRW/tHFAOlIdnFSrt3BLPvSOK7R3bVAskZQ0N4FTmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/replay/node_modules/@sentry/utils": { + "version": "7.99.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.99.0.tgz", + "integrity": "sha512-cYZy5WNTkWs5GgggGnjfGqC44CWir0pAv4GVVSx0fsup4D4pMKBJPrtub15f9uC+QkUf3vVkqwpBqeFxtmJQTQ==", + "dependencies": { + "@sentry/types": "7.99.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@sentry/tracing": { "version": "7.40.0", "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.40.0.tgz", @@ -4381,6 +4710,27 @@ "node": ">= 8" } }, + "node_modules/@sentry/webpack-plugin/node_modules/@sentry/cli": { + "version": "1.77.3", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.77.3.tgz", + "integrity": "sha512-c3eDqcDRmy4TFz2bFU5Y6QatlpoBPPa8cxBooaS4aMQpnIdLYPF1xhyyiW0LQlDUNc3rRjNF7oN5qKoaRoMTQQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "https-proxy-agent": "^5.0.0", + "mkdirp": "^0.5.5", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -6112,12 +6462,21 @@ "@apollo/client": "^3.0.0" } }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "license": "ISC" + "node_modules/apollo-link-sentry": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-3.3.0.tgz", + "integrity": "sha512-wLffWmo5sRw3rHN1Ck6azM0oxObvtaBBf3AC8cLX4SxhyjmkRIagGDji6CFkyAhxupPz0b9/H1u4Ocx+63lNug==", + "dependencies": { + "deepmerge": "^4.2.2", + "dot-prop": "^6.0.0", + "tslib": "^2.0.3", + "zen-observable-ts": "^1.2.5" + }, + "peerDependencies": { + "@apollo/client": "^3.2.3", + "@sentry/browser": "^7.41.0", + "graphql": "15 - 16" + } }, "node_modules/arch": { "version": "2.2.0", @@ -6140,17 +6499,6 @@ ], "license": "MIT" }, - "node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dev": true, - "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -7571,16 +7919,6 @@ "node": ">=4" } }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -7743,13 +8081,6 @@ "node": ">=0.8" } }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true, - "license": "ISC" - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -8911,13 +9242,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/denque": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", @@ -9218,6 +9542,28 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.0.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", @@ -10464,6 +10810,8 @@ }, "node_modules/eventemitter2": { "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", "dev": true, "license": "MIT" }, @@ -11283,74 +11631,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", - "dev": true, - "license": "ISC", - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/gauge/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gauge/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11753,13 +12033,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true, - "license": "ISC" - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -15744,19 +16017,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -15769,16 +16029,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nwsapi": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", @@ -17794,7 +18044,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -20526,13 +20775,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -22110,19 +22352,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/typescript-compare": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", @@ -23080,16 +23309,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", diff --git a/client/package.json b/client/package.json index 602f30e12..a93f91fac 100644 --- a/client/package.json +++ b/client/package.json @@ -9,12 +9,14 @@ "@craco/craco": "^7.0.0", "@fingerprintjs/fingerprintjs": "^3.4.2", "@jsreport/browser-client": "^3.1.0", - "@sentry/react": "^7.40.0", + "@sentry/cli": "^2.27.0", + "@sentry/react": "^7.99.0", "@sentry/tracing": "^7.40.0", "@splitsoftware/splitio-react": "^1.8.1", "@tanem/react-nprogress": "^5.0.8", "antd": "^4.24.8", "apollo-link-logger": "^2.0.1", + "apollo-link-sentry": "^3.3.0", "axios": "^1.3.4", "craco-less": "^2.0.0", "dinero.js": "^1.9.1", @@ -88,13 +90,14 @@ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "craco start", - "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build", + "build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps", "build:test": "env-cmd -f .env.test npm run build", "build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'", "buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build", "test": "cypress open", "eject": "react-scripts eject", - "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ." + "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .", + "sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build" }, "eslintConfig": { "extends": [ diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index 74f6ba4df..ffbe34c10 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -8,6 +8,7 @@ import { useTranslation } from "react-i18next"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import client from "../utils/GraphQLClient"; import App from "./App"; +import * as Sentry from "@sentry/react"; moment.locale("en-US"); @@ -18,7 +19,7 @@ export const factory = SplitSdk({ }, }); -export default function AppContainer() { +function AppContainer() { const { t } = useTranslation(); return ( @@ -42,3 +43,5 @@ export default function AppContainer() { ); } + +export default Sentry.withProfiler(AppContainer); diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 618366db4..4f074e5e9 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -22,6 +22,7 @@ import { } from "../redux/user/user.selectors"; import PrivateRoute from "../utils/private-route"; import "./App.styles.scss"; +import handleBeta from "../utils/handleBeta"; const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component") @@ -57,7 +58,6 @@ export function App({ if (!navigator.onLine) { setOnline(false); } - checkUserSession(); }, [checkUserSession, setOnline]); @@ -73,6 +73,7 @@ export function App({ window.addEventListener("online", function (e) { setOnline(true); }); + useEffect(() => { if (currentUser.authorized && bodyshop) { client.setAttribute("imexshopid", bodyshop.imexshopid); @@ -107,6 +108,8 @@ export function App({ /> ); + handleBeta(); + return ( }> diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx index e92bb175b..12ca80301 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect"; import { DELETE_BILL_LINE, INSERT_NEW_BILL_LINES, - UPDATE_BILL_LINE + UPDATE_BILL_LINE, } from "../../graphql/bill-lines.queries"; import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; @@ -20,6 +20,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AlertComponent from "../alert/alert.component"; import BillFormContainer from "../bill-form/bill-form.container"; import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; +import BillPrintButton from "../bill-print-button/bill-print-button.component"; import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; @@ -176,7 +177,7 @@ export function BillDetailEditcontainer({ extra={ - + form.submit()} diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index 617b9e603..c106a8c4c 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -94,6 +94,7 @@ function BillEnterModalContainer({ location, outstanding_returns, inventory, + federal_tax_exempt, ...remainingValues } = values; diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 44b8cd815..ba9f937ba 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -79,6 +79,20 @@ export function BillFormComponent({ }); }; + const handleFederalTaxExemptSwitchToggle = (checked) => { + // Early gate + if (!checked) return; + const values = form.getFieldsValue("billlines"); + // Gate bill lines + if (!values?.billlines?.length) return; + + const billlines = values.billlines.map((b) => { + b.applicable_taxes.federal = false; + return b; + }); + form.setFieldsValue({ billlines }); + }; + useEffect(() => { if (job) form.validateFields(["is_credit_memo"]); }, [job, form]); @@ -387,7 +401,16 @@ export function BillFormComponent({ > - + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( + + + + ) : null} + {() => { const values = form.getFieldsValue([ "billlines", @@ -405,7 +428,7 @@ export function BillFormComponent({ totals = CalculateBillTotal(values); if (!!totals) return ( -
+
{ + setLoading(true); + try { + await GenerateDocument( + { + name: Templates.parts_invoice_label_single.key, + variables: { + id: billid, + }, + }, + {}, + "p" + ); + } catch (e) { + console.warn("Warning: Error generating a document."); + } + setLoading(false); + }; + + return ( + + + + ); +} diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 1503bf46f..110ea2ea3 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -4,6 +4,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component"; import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component"; import ChatLabelComponent from "../chat-label/chat-label.component"; +import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; export default function ChatConversationTitle({ conversation }) { @@ -13,6 +14,7 @@ export default function ChatConversationTitle({ conversation }) { {conversation && conversation.phone_num} + ({ + setEmailOptions: (e) => dispatch(setEmailOptions(e)), +}); + +export function ChatPrintButton({ conversation }) { + const [loading, setLoading] = useState(false); + + return ( + + { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id }, + }, + { + subject: TemplateList("messaging").conversation_list.subject, + }, + "p", + conversation.id + ); + setLoading(false); + }} + /> + { + setLoading(true); + GenerateDocument( + { + name: TemplateList("messaging").conversation_list.key, + variables: { id: conversation.id }, + }, + { + subject: TemplateList("messaging").conversation_list.subject, + }, + "e", + conversation.id + ); + setLoading(false); + }} + /> + {loading && } + + ); +} +export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton); diff --git a/client/src/components/contract-cars/contract-cars.component.jsx b/client/src/components/contract-cars/contract-cars.component.jsx index 541d6d7c3..6162ae7af 100644 --- a/client/src/components/contract-cars/contract-cars.component.jsx +++ b/client/src/components/contract-cars/contract-cars.component.jsx @@ -35,6 +35,15 @@ export default function ContractsCarsComponent({ state.sortedInfo.columnKey === "status" && state.sortedInfo.order, render: (text, record) =>
{t(record.status)}
, }, + { + title: t("courtesycars.fields.readiness"), + dataIndex: "readiness", + key: "readiness", + sorter: (a, b) => alphaSort(a.readiness, b.readiness), + sortOrder: + state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, + render: (text, record) => t(record.readiness), + }, { title: t("courtesycars.fields.year"), dataIndex: "year", diff --git a/client/src/components/contract-form/contract-form.component.jsx b/client/src/components/contract-form/contract-form.component.jsx index f41933625..6f0125803 100644 --- a/client/src/components/contract-form/contract-form.component.jsx +++ b/client/src/components/contract-form/contract-form.component.jsx @@ -68,6 +68,30 @@ export default function ContractFormComponent({ )} + {create && ( + p.scheduledreturn !== c.scheduledreturn} + > + {() => { + const insuranceOver = + selectedCar && + selectedCar.insuranceexpires && + moment(selectedCar.insuranceexpires) + .endOf("day") + .isBefore(moment(form.getFieldValue("scheduledreturn"))); + if (insuranceOver) + return ( + + + + {t("contracts.labels.insuranceexpired")} + + + ); + return <>; + }} + + )} {() => { const mileageOver = - selectedCar && - selectedCar.nextservicekm <= form.getFieldValue("kmstart"); - + selectedCar && selectedCar.nextservicekm + ? selectedCar.nextservicekm <= form.getFieldValue("kmstart") + : false; const dueForService = selectedCar && selectedCar.nextservicedate && - moment(selectedCar.nextservicedate).isBefore( - moment(form.getFieldValue("scheduledreturn")) - ); - + moment(selectedCar.nextservicedate) + .endOf("day") + .isSameOrBefore( + moment(form.getFieldValue("scheduledreturn")) + ); if (mileageOver || dueForService) return ( @@ -117,7 +142,6 @@ export default function ContractFormComponent({ ); - return <>; }} diff --git a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx index d5315ed3a..f9a32dfee 100644 --- a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx +++ b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"; import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries"; import { DateFormatter } from "../../utils/DateFormatter"; import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component"; +import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component"; //import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; @@ -213,6 +214,9 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { > + + +
{() => { const nextservicekm = form.getFieldValue("nextservicekm"); - const mileageOver = - nextservicekm && nextservicekm <= form.getFieldValue("mileage"); + const mileageOver = nextservicekm + ? nextservicekm <= form.getFieldValue("mileage") + : false; if (mileageOver) return ( diff --git a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx index 2f9226b83..6b561d12a 100644 --- a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx +++ b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx @@ -34,6 +34,32 @@ const CourtesyCarFuelComponent = (props, ref) => { step={null} style={{ marginLeft: "2rem", marginRight: "2rem" }} {...props} + tooltip={{ + formatter: (value) => { + switch (value) { + case 0: + return t("courtesycars.labels.fuel.empty"); + case 13: + return t("courtesycars.labels.fuel.18"); + case 25: + return t("courtesycars.labels.fuel.14"); + case 38: + return t("courtesycars.labels.fuel.38"); + case 50: + return t("courtesycars.labels.fuel.12"); + case 63: + return t("courtesycars.labels.fuel.58"); + case 75: + return t("courtesycars.labels.fuel.34"); + case 88: + return t("courtesycars.labels.fuel.78"); + case 100: + return t("courtesycars.labels.fuel.full"); + default: + return value; + } + }, + }} /> ); }; diff --git a/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx b/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx new file mode 100644 index 000000000..9dd1fd514 --- /dev/null +++ b/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx @@ -0,0 +1,35 @@ +import { Select } from "antd"; +import React, { forwardRef, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +const { Option } = Select; + +const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => { + const [option, setOption] = useState(value); + const { t } = useTranslation(); + + useEffect(() => { + if (value !== option && onChange) { + onChange(option); + } + }, [value, option, onChange]); + + return ( + + ); +}; +export default forwardRef(CourtesyCarReadinessComponent); diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index ab85f95bc..e81a71e3e 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -72,18 +72,34 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, render: (text, record) => { - const { nextservicedate, nextservicekm, mileage } = record; + const { nextservicedate, nextservicekm, mileage, insuranceexpires } = + record; - const mileageOver = nextservicekm <= mileage; + const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const dueForService = - nextservicedate && moment(nextservicedate).isBefore(moment()); + nextservicedate && + moment(nextservicedate).endOf("day").isSameOrBefore(moment()); + + const insuranceOver = + insuranceexpires && + moment(insuranceexpires).endOf("day").isBefore(moment()); return ( {t(record.status)} - {(mileageOver || dueForService) && ( - + {(mileageOver || dueForService || insuranceOver) && ( + )} @@ -91,6 +107,26 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { ); }, }, + { + title: t("courtesycars.fields.readiness"), + dataIndex: "readiness", + key: "readiness", + sorter: (a, b) => alphaSort(a.readiness, b.readiness), + filters: [ + { + text: t("courtesycars.readiness.ready"), + value: "courtesycars.readiness.ready", + }, + { + text: t("courtesycars.readiness.notready"), + value: "courtesycars.readiness.notready", + }, + ], + onFilter: (value, record) => value.includes(record.readiness), + sortOrder: + state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order, + render: (text, record) => t(record.readiness), + }, { title: t("courtesycars.fields.year"), dataIndex: "year", @@ -131,6 +167,36 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { sortOrder: state.sortedInfo.columnKey === "plate" && state.sortedInfo.order, }, + { + title: t("courtesycars.fields.fuel"), + dataIndex: "fuel", + key: "fuel", + sorter: (a, b) => alphaSort(a.fuel, b.fuel), + sortOrder: + state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order, + render: (text, record) => { + switch (record.fuel) { + case 100: + return t("courtesycars.labels.fuel.full"); + case 88: + return t("courtesycars.labels.fuel.78"); + case 63: + return t("courtesycars.labels.fuel.58"); + case 50: + return t("courtesycars.labels.fuel.12"); + case 38: + return t("courtesycars.labels.fuel.34"); + case 25: + return t("courtesycars.labels.fuel.14"); + case 13: + return t("courtesycars.labels.fuel.18"); + case 0: + return t("courtesycars.labels.fuel.empty"); + default: + return record.fuel; + } + }, + }, { title: t("courtesycars.labels.outwith"), dataIndex: "outwith", diff --git a/client/src/components/csi-response-form/csi-response-form.container.jsx b/client/src/components/csi-response-form/csi-response-form.container.jsx index a1882b09a..d7a565608 100644 --- a/client/src/components/csi-response-form/csi-response-form.container.jsx +++ b/client/src/components/csi-response-form/csi-response-form.container.jsx @@ -5,6 +5,7 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries"; +import { DateFormatter } from "../../utils/DateFormatter"; import AlertComponent from "../alert/alert.component"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; @@ -44,6 +45,13 @@ export default function CsiResponseFormContainer() { readOnly componentList={data.csi_by_pk.csiquestion.config} /> + {data.csi_by_pk.validuntil ? ( + <> + {t("csi.fields.validuntil")} + {": "} + {data.csi_by_pk.validuntil} + + ) : null} ); diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx index f73e35512..3d1a3b864 100644 --- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx +++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx @@ -5,9 +5,11 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useHistory, useLocation } from "react-router-dom"; import { DateFormatter } from "../../utils/DateFormatter"; -import { alphaSort } from "../../utils/sorters"; -import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, dateSort } from "../../utils/sorters"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; export default function CsiResponseListPaginated({ refetch, @@ -16,23 +18,23 @@ export default function CsiResponseListPaginated({ total, }) { const search = queryString.parse(useLocation().search); - const { responseid, page, sortcolumn, sortorder } = search; + const { responseid } = search; const history = useHistory(); + const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, filteredInfo: { text: "" }, + page: "", }); - const { t } = useTranslation(); const columns = [ { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", - width: "8%", sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), - sortOrder: sortcolumn === "ro_number" && sortorder, - + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( {record.job.ro_number || t("general.labels.na")} @@ -41,15 +43,18 @@ export default function CsiResponseListPaginated({ }, { title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln), - width: "25%", - sortOrder: sortcolumn === "owner" && sortorder, + dataIndex: "owner_name", + key: "owner_name", + sorter: (a, b) => + alphaSort( + OwnerNameDisplayFunction(a.job), + OwnerNameDisplayFunction(b.job) + ), + sortOrder: + state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order, render: (text, record) => { - return record.job.owner ? ( - + return record.job.ownerid ? ( + ) : ( @@ -64,9 +69,9 @@ export default function CsiResponseListPaginated({ dataIndex: "completedon", key: "completedon", ellipsis: true, - sorter: (a, b) => a.completedon - b.completedon, - width: "25%", - sortOrder: sortcolumn === "completedon" && sortorder, + sorter: (a, b) => dateSort(a.completedon, b.completedon), + sortOrder: + state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order, render: (text, record) => { return record.completedon ? ( {record.completedon} @@ -76,11 +81,12 @@ export default function CsiResponseListPaginated({ ]; const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); - search.page = pagination.current; - search.sortcolumn = sorter.columnKey; - search.sortorder = sorter.order; - history.push({ search: queryString.stringify(search) }); + setState({ + ...state, + filteredInfo: filters, + sortedInfo: sorter, + page: pagination.current, + }); }; const handleOnRowClick = (record) => { @@ -108,7 +114,7 @@ export default function CsiResponseListPaginated({ pagination={{ position: "top", pageSize: pageLimit, - current: parseInt(page || 1), + current: parseInt(state.page || 1), total: total, }} columns={columns} @@ -122,13 +128,6 @@ export default function CsiResponseListPaginated({ selectedRowKeys: [responseid], type: "radio", }} - onRow={(record, rowIndex) => { - return { - onClick: (event) => { - handleOnRowClick(record); - }, // click row - }; - }} /> ); diff --git a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx index 74609ca49..fdf083a40 100644 --- a/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx @@ -3,21 +3,32 @@ import { ExclamationCircleFilled, PauseCircleOutlined, } from "@ant-design/icons"; -import { Card, Space, Table, Tooltip } from "antd"; +import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import moment from "moment"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; +import { TimeFormatter } from "../../../utils/DateFormatter"; +import { onlyUnique } from "../../../utils/arrayHelper"; +import { alphaSort, dateSort } from "../../../utils/sorters"; +import useLocalStorage from "../../../utils/useLocalStorage"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; -import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../../owner-name-display/owner-name-display.component"; import DashboardRefreshRequired from "../refresh-required.component"; -import {pageLimit} from "../../../utils/config"; export default function DashboardScheduledInToday({ data, ...cardProps }) { const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, + filteredInfo: {}, }); + const [isTvModeScheduledIn, setIsTvModeScheduledIn] = useLocalStorage( + "isTvModeScheduledIn", + false + ); + if (!data) return null; if (!data.scheduled_in_today) return ; @@ -31,6 +42,12 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { alt_transport: item.job.alt_transport, clm_no: item.job.clm_no, jobid: item.job.jobid, + joblines_body: item.job.joblines + .filter((l) => l.mod_lbr_ty !== "LAR") + .reduce((acc, val) => acc + val.mod_lb_hrs, 0), + joblines_ref: item.job.joblines + .filter((l) => l.mod_lbr_ty === "LAR") + .reduce((acc, val) => acc + val.mod_lb_hrs, 0), ins_co_nm: item.job.ins_co_nm, iouparent: item.job.iouparent, ownerid: item.job.ownerid, @@ -49,7 +66,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { v_vin: item.job.v_vin, vehicleid: item.job.vehicleid, note: item.note, - start: moment(item.start).format("hh:mm a"), + start: item.start, title: item.title, }; appt.push(i); @@ -59,11 +76,192 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { return new moment(a.start) - new moment(b.start); }); - const columns = [ + const tvFontSize = 16; + const tvFontWeight = "bold"; + + const tvColumns = [ + { + title: t("appointments.fields.time"), + dataIndex: "start", + key: "start", + ellipsis: true, + sorter: (a, b) => dateSort(a.start, b.start), + sortOrder: + state.sortedInfo.columnKey === "start" && state.sortedInfo.order, + render: (text, record) => ( + + {record.start} + + ), + }, { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()} + > + + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && ( + + )} + {record.iouparent && ( + + + + )} + + + + ), + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()} + > + + + + + ) : ( + + + + ); + }, + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()} + > + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + + ) : ( + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + ); + }, + }, + { + title: t("appointments.fields.alt_transport"), + dataIndex: "alt_transport", + key: "alt_transport", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "alt_transport" && + state.sortedInfo.order, + filters: + (appt && + appt + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.alt_transport), + render: (text, record) => ( + + {record.alt_transport} + + ), + }, + { + title: t("jobs.fields.lab"), + dataIndex: "joblines_body", + key: "joblines_body", + sorter: (a, b) => a.joblines_body - b.joblines_body, + sortOrder: + state.sortedInfo.columnKey === "joblines_body" && + state.sortedInfo.order, + align: "right", + render: (text, record) => ( + + {record.joblines_body.toFixed(1)} + + ), + }, + { + title: t("jobs.fields.lar"), + dataIndex: "joblines_ref", + key: "joblines_ref", + sorter: (a, b) => a.joblines_ref - b.joblines_ref, + sortOrder: + state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, + align: "right", + render: (text, record) => ( + + {record.joblines_ref.toFixed(1)} + + ), + }, + ]; + + const columns = [ + { + title: t("appointments.fields.time"), + dataIndex: "start", + key: "start", + ellipsis: true, + sorter: (a, b) => dateSort(a.start, b.start), + sortOrder: + state.sortedInfo.columnKey === "start" && state.sortedInfo.order, + render: (text, record) => {record.start}, + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => { return record.ownerid ? ( ( - - ), - }, - { - title: t("jobs.fields.ownr_ph2"), - dataIndex: "ownr_ph2", - key: "ownr_ph2", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - + + + + ), }, { @@ -134,7 +328,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { ellipsis: true, responsive: ["md"], render: (text, record) => ( - + {record.ownr_ea} ), }, { @@ -142,6 +336,15 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) { dataIndex: "vehicle", key: "vehicle", ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => { return record.vehicleid ? ( alphaSort(a.ins_co_nm, b.ins_co_nm), + sortOrder: + state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, + filters: + (appt && + appt + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Ins. Co.*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm), }, { title: t("appointments.fields.alt_transport"), dataIndex: "alt_transport", key: "alt_transport", ellipsis: true, - responsive: ["md"], + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "alt_transport" && + state.sortedInfo.order, + filters: + (appt && + appt + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.alt_transport), }, ]; - const handleTableChange = (sorter) => { - setState({ ...state, sortedInfo: sorter }); + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; return ( + {t("general.labels.tvmode")} + setIsTvModeScheduledIn(!isTvModeScheduledIn)} + defaultChecked={isTvModeScheduledIn} + /> + + } {...cardProps} >
@@ -220,6 +460,10 @@ export const DashboardScheduledInTodayGql = ` alt_transport clm_no jobid: id + joblines(where: {removed: {_eq: false}}) { + mod_lb_hrs + mod_lbr_ty + } ins_co_nm iouparent ownerid diff --git a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx index 0407e3aad..c5ac05f58 100644 --- a/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx +++ b/client/src/components/dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx @@ -3,37 +3,272 @@ import { ExclamationCircleFilled, PauseCircleOutlined, } from "@ant-design/icons"; -import { Card, Space, Table, Tooltip } from "antd"; +import { Card, Space, Switch, Table, Tooltip, Typography } from "antd"; import moment from "moment"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; +import { TimeFormatter } from "../../../utils/DateFormatter"; +import { onlyUnique } from "../../../utils/arrayHelper"; +import { alphaSort, dateSort } from "../../../utils/sorters"; +import useLocalStorage from "../../../utils/useLocalStorage"; import ChatOpenButton from "../../chat-open-button/chat-open-button.component"; -import OwnerNameDisplay from "../../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../../owner-name-display/owner-name-display.component"; import DashboardRefreshRequired from "../refresh-required.component"; -import {pageLimit} from "../../../utils/config"; export default function DashboardScheduledOutToday({ data, ...cardProps }) { const { t } = useTranslation(); const [state, setState] = useState({ sortedInfo: {}, + filteredInfo: {}, }); + const [isTvModeScheduledOut, setIsTvModeScheduledOut] = useLocalStorage( + "isTvModeScheduledOut", + false + ); + if (!data) return null; if (!data.scheduled_out_today) return ; data.scheduled_out_today.forEach((item) => { - item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a") + item.joblines_body = item.joblines + ? item.joblines + .filter((l) => l.mod_lbr_ty !== "LAR") + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + : 0; + item.joblines_ref = item.joblines + ? item.joblines + .filter((l) => l.mod_lbr_ty === "LAR") + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + : 0; }); data.scheduled_out_today.sort(function (a, b) { return new Date(a.scheduled_completion) - new Date(b.scheduled_completion); }); - const columns = [ + const tvFontSize = 18; + const tvFontWeight = "bold"; + + const tvColumns = [ + { + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", + ellipsis: true, + sorter: (a, b) => + dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: + state.sortedInfo.columnKey === "scheduled_completion" && + state.sortedInfo.order, + render: (text, record) => ( + + {record.scheduled_completion} + + ), + }, { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()} + > + + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && ( + + )} + {record.iouparent && ( + + + + )} + + + + ), + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + sorter: (a, b) => + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()} + > + + + + + ) : ( + + + + ); + }, + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()} + > + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + + ) : ( + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""}`} + ); + }, + }, + { + title: t("appointments.fields.alt_transport"), + dataIndex: "alt_transport", + key: "alt_transport", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "alt_transport" && + state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.alt_transport), + render: (text, record) => ( + + {record.alt_transport} + + ), + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + ellipsis: true, + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.status), + render: (text, record) => ( + + {record.status} + + ), + }, + { + title: t("jobs.fields.lab"), + dataIndex: "joblines_body", + key: "joblines_body", + sorter: (a, b) => a.joblines_body - b.joblines_body, + sortOrder: + state.sortedInfo.columnKey === "joblines_body" && + state.sortedInfo.order, + align: "right", + render: (text, record) => ( + + {record.joblines_body.toFixed(1)} + + ), + }, + { + title: t("jobs.fields.lar"), + dataIndex: "joblines_ref", + key: "joblines_ref", + sorter: (a, b) => a.joblines_ref - b.joblines_ref, + sortOrder: + state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order, + align: "right", + render: (text, record) => ( + + {record.joblines_ref.toFixed(1)} + + ), + }, + ]; + + const columns = [ + { + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", + ellipsis: true, + sorter: (a, b) => + dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: + state.sortedInfo.columnKey === "scheduled_completion" && + state.sortedInfo.order, + render: (text, record) => ( + {record.scheduled_completion} + ), + }, + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, render: (text, record) => ( + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, render: (text, record) => { return record.ownerid ? ( ( - - ), - }, - { - title: t("jobs.fields.ownr_ph2"), - dataIndex: "ownr_ph2", - key: "ownr_ph2", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - + + + + ), }, { @@ -104,7 +335,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { ellipsis: true, responsive: ["md"], render: (text, record) => ( - + {record.ownr_ea} ), }, { @@ -112,6 +343,15 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) { dataIndex: "vehicle", key: "vehicle", ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: + state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, render: (text, record) => { return record.vehicleid ? ( alphaSort(a.ins_co_nm, b.ins_co_nm), + sortOrder: + state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Ins. Co.*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm), }, { title: t("appointments.fields.alt_transport"), dataIndex: "alt_transport", key: "alt_transport", ellipsis: true, - responsive: ["md"], + sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport), + sortOrder: + state.sortedInfo.columnKey === "alt_transport" && + state.sortedInfo.order, + filters: + (data.scheduled_out_today && + data.scheduled_out_today + .map((j) => j.alt_transport) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Alt. Transport*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.alt_transport), }, ]; - const handleTableChange = (sorter) => { - setState({ ...state, sortedInfo: sorter }); + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; return ( + {t("general.labels.tvmode")} + setIsTvModeScheduledOut(!isTvModeScheduledOut)} + defaultChecked={isTvModeScheduledOut} + /> + + } {...cardProps} >
@@ -188,6 +465,10 @@ export const DashboardScheduledOutTodayGql = ` alt_transport clm_no jobid: id + joblines(where: {removed: {_eq: false}}) { + mod_lb_hrs + mod_lbr_ty + } ins_co_nm iouparent ownerid @@ -200,6 +481,7 @@ export const DashboardScheduledOutTodayGql = ` production_vars ro_number scheduled_completion + status suspended v_make_desc v_model_desc diff --git a/client/src/components/dashboard-grid/dashboard-grid.component.jsx b/client/src/components/dashboard-grid/dashboard-grid.component.jsx index d24dd5641..7012d943b 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.component.jsx +++ b/client/src/components/dashboard-grid/dashboard-grid.component.jsx @@ -275,26 +275,22 @@ const componentList = { h: 2, }, ScheduleInToday: { - label: i18next.t("dashboard.titles.scheduledintoday", { - date: moment().startOf("day").format("MM/DD/YYYY"), - }), + label: i18next.t("dashboard.titles.scheduledintoday"), component: DashboardScheduledInToday, gqlFragment: DashboardScheduledInTodayGql, - minW: 10, + minW: 6, minH: 2, w: 10, - h: 2, + h: 3, }, ScheduleOutToday: { - label: i18next.t("dashboard.titles.scheduledouttoday", { - date: moment().startOf("day").format("MM/DD/YYYY"), - }), + label: i18next.t("dashboard.titles.scheduledouttoday"), component: DashboardScheduledOutToday, gqlFragment: DashboardScheduledOutTodayGql, - minW: 10, + minW: 6, minH: 2, w: 10, - h: 2, + h: 3, }, }; @@ -306,8 +302,7 @@ const createDashboardQuery = (state) => { .map((item, index) => componentList[item.i].gqlFragment || "") .join(""); return gql` - query QUERY_DASHBOARD_DETAILS { - ${componentBasedAdditions || ""} + query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""} monthly_sales: jobs(where: {_and: [ { voided: {_eq: false}}, {date_invoiced: {_gte: "${moment() @@ -317,11 +312,11 @@ const createDashboardQuery = (state) => { .endOf("month") .endOf("day") .toISOString()}"}}]}) { - id - ro_number - date_invoiced - job_totals - rate_la1 + id + ro_number + date_invoiced + job_totals + rate_la1 rate_la2 rate_la3 rate_la4 @@ -344,43 +339,42 @@ const createDashboardQuery = (state) => { rate_mapa rate_mash rate_matd - joblines(where: { removed: { _eq: false } }) { + joblines(where: { removed: { _eq: false } }) { id mod_lbr_ty mod_lb_hrs act_price part_qty part_type + } } - } - production_jobs: jobs(where: { inproduction: { _eq: true } }) { + production_jobs: jobs(where: { inproduction: { _eq: true } }) { + id + ro_number + ins_co_nm + job_totals + joblines(where: { removed: { _eq: false } }) { id - ro_number - ins_co_nm - job_totals - joblines(where: { removed: { _eq: false } }) { - id - mod_lbr_ty - mod_lb_hrs - act_price - part_qty - part_type - } - labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { - aggregate { - sum { - mod_lb_hrs - } + mod_lbr_ty + mod_lb_hrs + act_price + part_qty + part_type + } + labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) { + aggregate { + sum { + mod_lb_hrs } } - larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { - aggregate { - sum { - mod_lb_hrs - } + } + larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) { + aggregate { + sum { + mod_lb_hrs } } } } - `; + }`; }; diff --git a/client/src/components/dashboard-grid/dashboard-grid.styles.scss b/client/src/components/dashboard-grid/dashboard-grid.styles.scss index 62a3ae72b..140a1e1f3 100644 --- a/client/src/components/dashboard-grid/dashboard-grid.styles.scss +++ b/client/src/components/dashboard-grid/dashboard-grid.styles.scss @@ -128,7 +128,7 @@ height: 100%; width: 100%; .ant-card-body { - height: 80%; + height: calc(100% - 2rem); width: 100%; // // background-color: red; // height: 90%; diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx index babae0b9e..89a14e2a8 100644 --- a/client/src/components/error-boundary/error-boundary.component.jsx +++ b/client/src/components/error-boundary/error-boundary.component.jsx @@ -2,7 +2,7 @@ import { Button, Col, Collapse, Result, Row, Space } from "antd"; import React from "react"; import { withTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; - +import * as Sentry from "@sentry/react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { @@ -138,7 +138,6 @@ class ErrorBoundary extends React.Component { } } -export default connect( - mapStateToProps, - mapDispatchToProps -)(withTranslation()(ErrorBoundary)); +export default Sentry.withErrorBoundary( + connect(mapStateToProps, mapDispatchToProps)(withTranslation()(ErrorBoundary)) +); diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 8d180bdd1..d22e37d89 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -13,7 +13,7 @@ import Icon, { FileFilled, //GlobalOutlined, HomeFilled, - ImportOutlined, + ImportOutlined, InfoCircleOutlined, LineChartOutlined, PaperClipOutlined, PhoneOutlined, @@ -26,8 +26,8 @@ import Icon, { UserOutlined, } from "@ant-design/icons"; import { useTreatments } from "@splitsoftware/splitio-react"; -import { Layout, Menu } from "antd"; -import React from "react"; +import {Layout, Menu, Switch, Tooltip} from "antd"; +import React, {useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import { BsKanban } from "react-icons/bs"; import { @@ -52,6 +52,7 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; +import {handleBeta, setBeta, checkBeta} from "../../utils/handleBeta"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -102,9 +103,21 @@ function Header({ {}, bodyshop && bodyshop.imexshopid ); + const [betaSwitch, setBetaSwitch] = useState(false); const { t } = useTranslation(); + useEffect(() => { + const isBeta = checkBeta(); + setBetaSwitch(isBeta); + }, []); + + const betaSwitchChange = (checked) => { + setBeta(checked); + setBetaSwitch(checked); + handleBeta(); + } + return ( ))} + + + + Try the new ImEX Online + + + + ); diff --git a/client/src/components/job-create-iou/job-create-iou.component.jsx b/client/src/components/job-create-iou/job-create-iou.component.jsx index 8ebc8fb75..930f27e25 100644 --- a/client/src/components/job-create-iou/job-create-iou.component.jsx +++ b/client/src/components/job-create-iou/job-create-iou.component.jsx @@ -7,21 +7,31 @@ import { connect } from "react-redux"; import { useHistory } from "react-router"; import { createStructuredSelector } from "reselect"; import { UPDATE_JOB_LINES_IOU } from "../../graphql/jobs-lines.queries"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; import { CreateIouForJob } from "../jobs-detail-header-actions/jobs-detail-header-actions.duplicate.util"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser, + technician: selectTechnician, }); + const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(JobCreateIOU); -export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) { +export function JobCreateIOU({ + bodyshop, + currentUser, + job, + selectedJobLines, + technician, +}) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const client = useApolloClient(); @@ -79,13 +89,19 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines }) { title={t("jobs.labels.createiouwarning")} onConfirm={handleCreateIou} disabled={ - !selectedJobLines || selectedJobLines.length === 0 || !job.converted + !selectedJobLines || + selectedJobLines.length === 0 || + !job.converted || + technician } >
+ + + ) : ( + + {t('job_lifecycle.content.data_unavailable')} + + ) + ) : ( + + {t('job_lifecycle.content.loading')} + + )} + + ); +} + +export default JobLifecycleComponent; \ No newline at end of file diff --git a/client/src/components/job-lifecycle/job-lifecycle.styles.scss b/client/src/components/job-lifecycle/job-lifecycle.styles.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/components/job-scoreboard-add-button/job-scoreboard-add-button.component.jsx b/client/src/components/job-scoreboard-add-button/job-scoreboard-add-button.component.jsx index 0e1343438..68b8980ea 100644 --- a/client/src/components/job-scoreboard-add-button/job-scoreboard-add-button.component.jsx +++ b/client/src/components/job-scoreboard-add-button/job-scoreboard-add-button.component.jsx @@ -1,16 +1,16 @@ -import { useMutation, useLazyQuery } from "@apollo/client"; import { CheckCircleOutlined } from "@ant-design/icons"; +import { useLazyQuery, useMutation } from "@apollo/client"; import { Button, Card, Form, InputNumber, - notification, Popover, Space, + notification, } from "antd"; import moment from "moment"; -import React, { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { @@ -50,6 +50,7 @@ export default function ScoreboardAddButton({ const handleFinish = async (values) => { logImEXEvent("job_close_add_to_scoreboard"); + values.date = moment(values.date).format("YYYY-MM-DD"); setLoading(true); let result; @@ -177,7 +178,7 @@ export default function ScoreboardAddButton({ return acc + job.lbr_adjustments[val]; }, 0); form.setFieldsValue({ - date: new moment(), + date: moment(), bodyhrs: Math.round(v.bodyhrs * 10) / 10, painthrs: Math.round(v.painthrs * 10) / 10, }); diff --git a/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx b/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx index edef81343..54a020d37 100644 --- a/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx +++ b/client/src/components/jobs-admin-change-status/jobs-admin-change.status.component.jsx @@ -53,12 +53,14 @@ export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) { ); return ( - - - + + + + ); } diff --git a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx index 190778437..9d95f6e80 100644 --- a/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx +++ b/client/src/components/jobs-admin-delete-intake/jobs-admin-delete-intake.component.jsx @@ -1,34 +1,18 @@ import { useMutation } from "@apollo/client"; -import { Button, notification } from "antd"; -import { gql } from "@apollo/client"; +import { Button, Space, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { + DELETE_DELIVERY_CHECKLIST, + DELETE_INTAKE_CHECKLIST, +} from "../../graphql/jobs.queries"; + export default function JobAdminDeleteIntake({ job }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [deleteIntake] = useMutation(gql` - mutation DELETE_INTAKE($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { intakechecklist: null } - ) { - id - intakechecklist - } - } - `); - const [DELETE_DELIVERY] = useMutation(gql` - mutation DELETE_DELIVERY($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { deliverchecklist: null } - ) { - id - deliverchecklist - } - } - `); + const [deleteIntake] = useMutation(DELETE_INTAKE_CHECKLIST); + const [deleteDelivery] = useMutation(DELETE_DELIVERY_CHECKLIST); const handleDelete = async (values) => { setLoading(true); @@ -50,7 +34,7 @@ export default function JobAdminDeleteIntake({ job }) { const handleDeleteDelivery = async (values) => { setLoading(true); - const result = await DELETE_DELIVERY({ + const result = await deleteDelivery({ variables: { jobId: job.id }, }); @@ -68,12 +52,22 @@ export default function JobAdminDeleteIntake({ job }) { return ( <> - - + + + + ); } diff --git a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx index c47c30def..98f5c65d5 100644 --- a/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx +++ b/client/src/components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component.jsx @@ -1,5 +1,5 @@ -import { gql, useMutation } from "@apollo/client"; -import { Button, notification } from "antd"; +import { useMutation } from "@apollo/client"; +import { Button, Space, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -7,6 +7,11 @@ import moment from "moment"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries"; +import { + MARK_JOB_AS_EXPORTED, + MARK_JOB_AS_UNINVOICED, + MARK_JOB_FOR_REEXPORT, +} from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, @@ -35,58 +40,18 @@ export function JobAdminMarkReexport({ const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [insertExportLog] = useMutation(INSERT_EXPORT_LOG); - const [markJobForReexport] = useMutation(gql` - mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: null - status: "${bodyshop.md_ro_statuses.default_invoiced}" - } - ) { - id - date_exported - status - date_invoiced - } - } - `); - const [markJobExported] = useMutation(gql` - mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: $date_exported - status: "${bodyshop.md_ro_statuses.default_exported}" - } - ) { - id - date_exported - date_invoiced - status - } - } - `); - const [markJobUninvoiced] = useMutation(gql` - mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) { - update_jobs_by_pk( - pk_columns: { id: $jobId } - _set: { date_exported: null - date_invoiced: null - status: "${bodyshop.md_ro_statuses.default_delivered}" - } - ) { - id - date_exported - date_invoiced - status - } - } - `); + const [markJobForReexport] = useMutation(MARK_JOB_FOR_REEXPORT); + const [markJobExported] = useMutation(MARK_JOB_AS_EXPORTED); + const [markJobUninvoiced] = useMutation(MARK_JOB_AS_UNINVOICED); const handleMarkForExport = async () => { setLoading(true); const result = await markJobForReexport({ - variables: { jobId: job.id }, + variables: { + jobId: job.id, + default_invoiced: bodyshop.md_ro_statuses.default_invoiced, + }, }); if (!result.errors) { @@ -108,7 +73,11 @@ export function JobAdminMarkReexport({ const handleMarkExported = async () => { setLoading(true); const result = await markJobExported({ - variables: { jobId: job.id, date_exported: moment() }, + variables: { + jobId: job.id, + date_exported: moment(), + default_exported: bodyshop.md_ro_statuses.default_exported, + }, }); await insertExportLog({ @@ -144,7 +113,10 @@ export function JobAdminMarkReexport({ const handleUninvoice = async () => { setLoading(true); const result = await markJobUninvoiced({ - variables: { jobId: job.id }, + variables: { + jobId: job.id, + default_delivered: bodyshop.md_ro_statuses.default_delivered, + }, }); if (!result.errors) { @@ -165,27 +137,29 @@ export function JobAdminMarkReexport({ return ( <> - - - + + + + + ); } diff --git a/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx new file mode 100644 index 000000000..f1bda15ce --- /dev/null +++ b/client/src/components/jobs-admin-remove-ar/jobs-admin-remove-ar.component.jsx @@ -0,0 +1,65 @@ +import { useMutation } from "@apollo/client"; +import { Switch, notification } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { UPDATE_REMOVE_FROM_AR } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; + +const mapStateToProps = createStructuredSelector({}); +const mapDispatchToProps = (dispatch) => ({ + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR); + +export function JobsAdminRemoveAR({ insertAuditTrail, job }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [switchValue, setSwitchValue] = useState(job.remove_from_ar); + + const [mutationUpdateRemoveFromAR] = useMutation(UPDATE_REMOVE_FROM_AR); + + const handleChange = async (value) => { + setLoading(true); + const result = await mutationUpdateRemoveFromAR({ + variables: { jobId: job.id, remove_from_ar: value }, + }); + + if (!result.errors) { + notification["success"]({ message: t("jobs.successes.save") }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.admin_job_remove_from_ar(value), + }); + setSwitchValue(value); + } else { + notification["error"]({ + message: t("jobs.errors.saving", { + error: JSON.stringify(result.errors), + }), + }); + } + setLoading(false); + }; + + return ( + <> +
+
+ {t("jobs.labels.remove_from_ar")}: +
+
+ +
+
+ + ); +} diff --git a/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx b/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx index 7963fd05f..2094178c4 100644 --- a/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx +++ b/client/src/components/jobs-admin-unvoid/jobs-admin-unvoid.component.jsx @@ -1,9 +1,10 @@ -import { gql, useMutation } from "@apollo/client"; +import { useMutation } from "@apollo/client"; import { Button, notification } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { UNVOID_JOB } from "../../graphql/jobs.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, @@ -29,66 +30,17 @@ export function JobsAdminUnvoid({ }) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); - const [updateJob] = useMutation(gql` -mutation UNVOID_JOB($jobId: uuid!) { - update_jobs_by_pk(pk_columns: {id: $jobId}, _set: {voided: false, status: "${ - bodyshop.md_ro_statuses.default_imported - }", date_void: null}) { - id - date_void - voided - status - } - insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${ - currentUser.email - }", text: "${t("jobs.labels.unvoidnote")}"}) { - returning { - id - } - } -} - - `); - - // const result = await voidJob({ - // variables: { - // jobId: job.id, - // job: { - // status: bodyshop.md_ro_statuses.default_void, - // voided: true, - // }, - // note: [ - // { - // jobid: job.id, - // created_by: currentUser.email, - // audit: true, - // text: t("jobs.labels.voidnote", { - // date: moment().format("MM/DD/yyy"), - // time: moment().format("hh:mm a"), - // }), - // }, - // ], - // }, - // }); - - // if (!!!result.errors) { - // notification["success"]({ - // message: t("jobs.successes.voided"), - // }); - // //go back to jobs list. - // history.push(`/manage/`); - // } else { - // notification["error"]({ - // message: t("jobs.errors.voiding", { - // error: JSON.stringify(result.errors), - // }), - // }); - // } + const [mutationUnvoidJob] = useMutation(UNVOID_JOB); const handleUpdate = async (values) => { setLoading(true); - const result = await updateJob({ - variables: { jobId: job.id }, + const result = await mutationUnvoidJob({ + variables: { + jobId: job.id, + default_imported: bodyshop.md_ro_statuses.default_imported, + currentUserEmail: currentUser.email, + text: t("jobs.labels.unvoidnote"), + }, }); if (!result.errors) { @@ -110,8 +62,10 @@ mutation UNVOID_JOB($jobId: uuid!) { }; return ( - + <> + + ); } diff --git a/client/src/components/jobs-available-table/jobs-available-table.container.jsx b/client/src/components/jobs-available-table/jobs-available-table.container.jsx index 12d10baf9..bbad59845 100644 --- a/client/src/components/jobs-available-table/jobs-available-table.container.jsx +++ b/client/src/components/jobs-available-table/jobs-available-table.container.jsx @@ -6,10 +6,10 @@ import { useQuery, } from "@apollo/client"; import { useTreatments } from "@splitsoftware/splitio-react"; -import { Col, notification, Row } from "antd"; +import { Col, Row, notification } from "antd"; import Axios from "axios"; import Dinero from "dinero.js"; -import moment from "moment"; +import moment from "moment-business-days"; import queryString from "query-string"; import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -30,8 +30,8 @@ import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; -import confirmDialog from "../../utils/asyncConfirm"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import confirmDialog from "../../utils/asyncConfirm"; import CriticalPartsScan from "../../utils/criticalPartsScan"; import AlertComponent from "../alert/alert.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; @@ -73,7 +73,15 @@ export function JobsAvailableContainer({ const [selectedJob, setSelectedJob] = useState(null); const [selectedOwner, setSelectedOwner] = useState(null); - const [partsQueueToggle, setPartsQueueToggle] = useState(bodyshop.md_functionality_toggles.parts_queue_toggle); + const [partsQueueToggle, setPartsQueueToggle] = useState( + bodyshop.md_functionality_toggles.parts_queue_toggle + ); + const [updateSchComp, setSchComp] = useState({ + actual_in: moment(), + checked: false, + scheduled_completion: moment(), + automatic: false, + }); const [insertLoading, setInsertLoading] = useState(false); @@ -197,11 +205,16 @@ export function JobsAvailableContainer({ notification["error"]({ message: t("jobs.errors.creating", { error: err.message }), }); - refetch().catch(e => {console.error(`Something went wrong in jobs available table container - ${err.message || ''}`)}); + refetch().catch((e) => { + console.error( + `Something went wrong in jobs available table container - ${ + err.message || "" + }` + ); + }); setInsertLoading(false); setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); } - }; //Supplement scenario @@ -225,6 +238,22 @@ export function JobsAvailableContainer({ //IO-539 Check for Parts Rate on PAL for SGI use case. await CheckTaxRates(supp, bodyshop); + if (updateSchComp.checked === true) { + if (updateSchComp.automatic === true) { + const job_hrs = supp.joblines.data.reduce( + (acc, val) => acc + val.mod_lb_hrs, + 0 + ); + const num_days = job_hrs / bodyshop.target_touchtime; + supp.actual_in = updateSchComp.actual_in; + supp.scheduled_completion = moment( + updateSchComp.actual_in + ).businessAdd(num_days, "days"); + } else { + supp.scheduled_completion = updateSchComp.scheduled_completion; + } + } + delete supp.owner; delete supp.vehicle; delete supp.ins_co_nm; @@ -261,9 +290,9 @@ export function JobsAvailableContainer({ }, }); - setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); + setPartsQueueToggle(bodyshop.md_functionality_toggles.parts_queue_toggle); - if (CriticalPartsScanning.treatment === "on") { + if (CriticalPartsScanning.treatment === "on") { CriticalPartsScan(updateResult.data.update_jobs.returning[0].id); } if (updateResult.errors) { @@ -367,7 +396,6 @@ export function JobsAvailableContainer({ if (error) return ; - return (
diff --git a/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx b/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx index 2dea9a657..da2a1b1a5 100644 --- a/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx +++ b/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx @@ -19,10 +19,13 @@ export default function JobsCreateOwnerInfoNewComponent() { label={t("owners.fields.ownr_ln")} name={["owner", "data", "ownr_ln"]} rules={[ - { - required: state.owner.new, + ({ getFieldValue }) => ({ + required: + state.owner.new && + (!getFieldValue(["owner", "data", "ownr_co_nm"]) || + getFieldValue(["owner", "data", "ownr_co_nm"]) === ""), //message: t("general.validation.required"), - }, + }), ]} > @@ -31,10 +34,13 @@ export default function JobsCreateOwnerInfoNewComponent() { label={t("owners.fields.ownr_fn")} name={["owner", "data", "ownr_fn"]} rules={[ - { - required: state.owner.new, + ({ getFieldValue }) => ({ + required: + state.owner.new && + (!getFieldValue(["owner", "data", "ownr_co_nm"]) || + getFieldValue(["owner", "data", "ownr_co_nm"]) === ""), //message: t("general.validation.required"), - }, + }), ]} > @@ -51,6 +57,17 @@ export default function JobsCreateOwnerInfoNewComponent() { ({ + required: + state.owner.new && + (!getFieldValue(["owner", "data", "ownr_ln"]) || + !getFieldValue(["owner", "data", "ownr_fn"]) || + getFieldValue(["owner", "data", "ownr_ln"]) === "" || + getFieldValue(["owner", "data", "ownr_fn"]) === ""), + //message: t("general.validation.required"), + }), + ]} > diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index a5dd99f6f..70dbc033a 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -1,8 +1,8 @@ import { + BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled, - BranchesOutlined, } from "@ant-design/icons"; import { Card, Col, Row, Space, Tag, Tooltip } from "antd"; import React, { useState } from "react"; @@ -15,6 +15,7 @@ import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter"; +import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import DataLabel from "../data-label/data-label.component"; import JobAltTransportChange from "../job-at-change/job-at-change.component"; @@ -122,20 +123,23 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {job.cccontracts.length > 0 && ( - {job.cccontracts.map((c) => ( - {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`} + {job.cccontracts.map((c, index) => ( + + + {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`} + {index !== job.cccontracts.length - 1 ? "," : null} + + ))} )} - - - + {job.special_coverage_policy && ( @@ -160,19 +164,35 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { - {ownerTitle.length > 0 - ? ownerTitle - : t("owner.labels.noownerinfo")} - + disabled ? ( + <> + {ownerTitle.length > 0 + ? ownerTitle + : t("owner.labels.noownerinfo")} + + ) : ( + + {ownerTitle.length > 0 + ? ownerTitle + : t("owner.labels.noownerinfo")} + + ) } >
- + {disabled ? ( + {job.ownr_ph1} + ) : ( + + )} - + {disabled ? ( + {job.ownr_ph2} + ) : ( + + )} {`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${ @@ -180,7 +200,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { } ${job.ownr_st || ""} ${job.ownr_zip || ""}`} - {job.ownr_ea || ""} + {disabled ? ( + <>{job.ownr_ea || ""} + ) : job.ownr_ea ? ( + {job.ownr_ea} + ) : null} {job.owner?.tax_number && ( @@ -195,17 +219,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { style={{ height: "100%" }} title={ job.vehicle ? ( - - {vehicleTitle.length > 0 - ? vehicleTitle - : t("vehicles.labels.novehinfo")} - + disabled ? ( + <> + {vehicleTitle.length > 0 + ? vehicleTitle + : t("vehicles.labels.novehinfo")}{" "} + + ) : ( + + {vehicleTitle.length > 0 + ? vehicleTitle + : t("vehicles.labels.novehinfo")} + + ) ) : ( ) @@ -221,9 +247,19 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { {`${job.v_vin || t("general.labels.na")}`} + {bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? ( + job.v_vin?.length !== 17 ? ( + + ) : null + ) : null} + + + {job.regie_number || t("general.labels.na")} - + {job.vehicle && job.vehicle.notes && ( { setSelectedJob(props.id); + if (props.actual_in && props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: props.actual_in, + scheduled_completion: props.scheduled_completion, + }); + } else { + if (props.actual_in && !props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: props.actual_in, + scheduled_completion: moment(), + }); + } + if (!props.actual_in && props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: moment(), + scheduled_completion: moment(props.scheduled_completion), + }); + } + if (!props.actual_in && !props.scheduled_completion) { + setSchComp({ + ...updateSchComp, + actual_in: moment(), + scheduled_completion: moment(), + }); + } + } }, type: "radio", selectedRowKeys: [selectedJob], @@ -190,23 +252,58 @@ export default function JobsFindModalComponent({ }} /> - - setImportOptions({ - ...importOptions, - overrideHeaders: e.target.checked, - }) - } - > - {t("jobs.labels.override_header")} - - + + setImportOptions({ + ...importOptions, + overrideHeaders: e.target.checked, + }) + } + > + {t("jobs.labels.override_header")} + + setPartsQueueToggle(e.target.checked)} - > - {t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")} - + > + {t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")} + + + setSchComp({ ...updateSchComp, checked: e.target.checked }) + } + > + {t("jobs.labels.update_scheduled_completion")} + + {updateSchComp.checked === true ? ( + <> + {checkUTT === false ? ( + { + setSchComp({ ...updateSchComp, scheduled_completion: e }); + }} + /> + ) : null} + { + setCheckUTT(e.target.checked); + setSchComp({ + ...updateSchComp, + scheduled_completion: null, + automatic: true, + }); + }} + > + {t("jobs.labels.calc_scheuled_completion")} + + + ) : null} +
); } diff --git a/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx b/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx index 64dc8be9a..e6b4ab25e 100644 --- a/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx +++ b/client/src/components/jobs-find-modal/jobs-find-modal.container.jsx @@ -26,6 +26,8 @@ export default connect( modalSearchState, partsQueueToggle, setPartsQueueToggle, + updateSchComp, + setSchComp, ...modalProps }) { const { t } = useTranslation(); @@ -95,6 +97,8 @@ export default connect( modalSearchState={modalSearchState} partsQueueToggle={partsQueueToggle} setPartsQueueToggle={setPartsQueueToggle} + updateSchComp={updateSchComp} + setSchComp={setSchComp} /> ) : null} diff --git a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx index b5048d0ef..e247f5450 100644 --- a/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx +++ b/client/src/components/jobs-list-paginated/jobs-list-paginated.component.jsx @@ -10,9 +10,10 @@ import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { pageLimit } from "../../utils/config"; +import useLocalStorage from "../../utils/useLocalStorage"; import StartChatButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser bodyshop: selectBodyshop, @@ -25,6 +26,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { const search = queryString.parse(useLocation().search); const [openSearchResults, setOpenSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); + const [filter, setFilter] = useLocalStorage("filter_jobs_all", null); const { page, sortcolumn, sortorder } = search; const history = useHistory(); @@ -93,6 +95,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { render: (text, record) => { return record.status || t("general.labels.na"); }, + filteredValue: filter?.status || null, filters: bodyshop.md_ro_statuses.statuses.map((s) => { return { text: s, value: [s] }; }), @@ -189,6 +192,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) { } else { delete search.statusFilters; } + setFilter(filters); history.push({ search: queryString.stringify(search) }); }; diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index fb2e1daa6..68b3f3425 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -1,8 +1,8 @@ import { - SyncOutlined, - ExclamationCircleFilled, - PauseCircleOutlined, - BranchesOutlined, + BranchesOutlined, + ExclamationCircleFilled, + PauseCircleOutlined, + SyncOutlined, } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd"; @@ -14,382 +14,389 @@ import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import { onlyUnique } from "../../utils/arrayHelper"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import { alphaSort } from "../../utils/sorters"; +import { onlyUnique } from "../../utils/arrayHelper"; +import { alphaSort, statusSort } from "../../utils/sorters"; +import useLocalStorage from "../../utils/useLocalStorage"; import AlertComponent from "../alert/alert.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop, }); export function JobsList({ bodyshop }) { - const searchParams = queryString.parse(useLocation().search); - const { selected } = searchParams; - const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) - .filter((screen) => !!screen[1]) - .slice(-1)[0]; - const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, { - variables: { - statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], - }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); + const searchParams = queryString.parse(useLocation().search); + const { selected } = searchParams; + const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) + .filter((screen) => !!screen[1]) + .slice(-1)[0]; + const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, { + variables: { + statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], + }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: { text: "" }, - }); + const [state, setState] = useState({ sortedInfo: {} }); + const [filter, setFilter] = useLocalStorage("filter_jobs_list", null); - const { t } = useTranslation(); - const history = useHistory(); - const [searchText, setSearchText] = useState(""); + const { t } = useTranslation(); + const history = useHistory(); + const [searchText, setSearchText] = useState(""); - if (error) return ; + if (error) return ; - const jobs = data - ? searchText === "" - ? data.jobs - : data.jobs.filter( - (j) => - (j.ro_number || "") - .toString() - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.ownr_co_nm || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.comments || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.ownr_fn || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.ownr_ln || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || - (j.plate_no || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.v_model_desc || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.est_ct_fn || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.est_ct_ln || "") - .toLowerCase() - .includes(searchText.toLowerCase()) || - (j.v_make_desc || "") - .toLowerCase() - .includes(searchText.toLowerCase()) - ) - : []; + const jobs = data + ? searchText === "" + ? data.jobs + : data.jobs.filter( + (j) => + (j.ro_number || "") + .toString() + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_co_nm || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.comments || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.ownr_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) || + (j.plate_no || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.v_model_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.v_make_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) + ) + : []; - const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); - }; + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); + }; - const handleOnRowClick = (record) => { - if (record) { - if (record.id) { - history.push({ - search: queryString.stringify({ - ...searchParams, - selected: record.id, - }), - }); - } - } - }; + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + history.push({ + search: queryString.stringify({ + ...searchParams, + selected: record.id, + }), + }); + } + } + }; - const columns = [ - { - title: t("jobs.fields.ro_number"), - dataIndex: "ro_number", - key: "ro_number", - sorter: (a, b) => - parseInt((a.ro_number || "0").replace(/\D/g, "")) - - parseInt((b.ro_number || "0").replace(/\D/g, "")), - sortOrder: - state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - - render: (text, record) => ( - e.stopPropagation()} - > - - {record.ro_number || t("general.labels.na")} - {record.production_vars && record.production_vars.alert ? ( - - ) : null} - {record.suspended && ( - - )} - {record.iouparent && ( - - - - )} - - - ), - }, - { - title: t("jobs.fields.owner"), - dataIndex: "owner", - key: "owner", - ellipsis: true, - - responsive: ["md"], - sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), - sortOrder: - state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, - render: (text, record) => { - return record.ownerid ? ( - e.stopPropagation()} - > - - - ) : ( - + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => + parseInt((a.ro_number || "0").replace(/\D/g, "")) - + parseInt((b.ro_number || "0").replace(/\D/g, "")), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + e.stopPropagation()} + > + + {record.ro_number || t("general.labels.na")} + {record.production_vars && record.production_vars.alert ? ( + + ) : null} + {record.suspended && ( + + )} + {record.iouparent && ( + + + + )} + + + ), + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + responsive: ["md"], + sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.ownerid ? ( + e.stopPropagation()} + > + + + ) : ( + - ); + ); + }, + }, + { + title: t("jobs.fields.ownr_ph1"), + dataIndex: "ownr_ph1", + key: "ownr_ph1", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + ), + }, + { + title: t("jobs.fields.ownr_ph2"), + dataIndex: "ownr_ph2", + key: "ownr_ph2", + ellipsis: true, + responsive: ["md"], + render: (text, record) => ( + + ), + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + ellipsis: true, + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filteredValue: filter?.status || null, + filters: + (jobs && + jobs + .map((j) => j.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + }) + .sort((a, b) => + statusSort( + a.text, + b.text, + bodyshop.md_ro_statuses.active_statuses + ) + )) || + [], + onFilter: (value, record) => value.includes(record.status), + }, + + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + render: (text, record) => { + return record.vehicleid ? ( + e.stopPropagation()} + > + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("vehicles.fields.plate_no"), + dataIndex: "plate_no", + key: "plate_no", + ellipsis: true, + + responsive: ["md"], + sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), + sortOrder: + state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, + }, + { + title: t("jobs.fields.clm_no"), + dataIndex: "clm_no", + key: "clm_no", + ellipsis: true, + responsive: ["md"], + sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), + sortOrder: + state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, + render: (text, record) => + `${record.clm_no || ""}${ + record.po_number ? ` (PO: ${record.po_number})` : "" + }`, + }, + { + title: t("jobs.fields.ins_co_nm"), + dataIndex: "ins_co_nm", + key: "ins_co_nm", + ellipsis: true, + filteredValue: filter?.ins_co_nm || null, + filters: + (jobs && + jobs + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Ins. Co.*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm), + responsive: ["md"], + }, + { + title: t("jobs.fields.clm_total"), + dataIndex: "clm_total", + key: "clm_total", + responsive: ["md"], + ellipsis: true, + sorter: (a, b) => a.clm_total - b.clm_total, + sortOrder: + state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, + render: (text, record) => ( + {record.clm_total} + ), + }, + { + title: t("jobs.labels.estimator"), + dataIndex: "jobs.labels.estimator", + key: "estimator", + ellipsis: true, + responsive: ["xl"], + filterSearch: true, + filteredValue: filter?.estimator || null, + filters: + (jobs && + jobs + .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Estimator*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => + value.includes( + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() + ), + render: (text, record) => + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), + }, + { + title: t("jobs.fields.comment"), + dataIndex: "comment", + key: "comment", + ellipsis: true, + responsive: ["md"], + }, + // { + // title: t("jobs.fields.owner_owing"), + // dataIndex: "owner_owing", + // key: "owner_owing", + // responsive: ["md"], + // render: (text, record) => ( + // {record.owner_owing} + // ), + // }, + ]; + + const scrollMapper = { + xs: true, + sm: true, + md: true, + lg: "100%", + xl: "100%", + xxl: "100%", + }; + + return ( + + + { + setSearchText(e.target.value); + }} + value={searchText} + enterButton + /> +
+ } + > +
{ + handleOnRowClick(record); + }, + selectedRowKeys: [selected], + type: "radio", + }} + onChange={handleTableChange} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); }, - }, - { - title: t("jobs.fields.ownr_ph1"), - dataIndex: "ownr_ph1", - key: "ownr_ph1", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - - ), - }, - { - title: t("jobs.fields.ownr_ph2"), - dataIndex: "ownr_ph2", - key: "ownr_ph2", - ellipsis: true, - responsive: ["md"], - render: (text, record) => ( - - ), - }, - - { - title: t("jobs.fields.status"), - dataIndex: "status", - key: "status", - ellipsis: true, - - sorter: (a, b) => alphaSort(a.status, b.status), - sortOrder: - state.sortedInfo.columnKey === "status" && state.sortedInfo.order, - filters: - (jobs && - jobs - .map((j) => j.status) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "No Status*", - value: [s], - }; - })) || - [], - onFilter: (value, record) => value.includes(record.status), - }, - - { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", - ellipsis: true, - render: (text, record) => { - return record.vehicleid ? ( - e.stopPropagation()} - > - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, - }, - { - title: t("vehicles.fields.plate_no"), - dataIndex: "plate_no", - key: "plate_no", - ellipsis: true, - - responsive: ["md"], - sorter: (a, b) => alphaSort(a.plate_no, b.plate_no), - sortOrder: - state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order, - }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - ellipsis: true, - responsive: ["md"], - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: - state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, - render: (text, record) => - `${record.clm_no || ""}${ - record.po_number ? ` (PO: ${record.po_number})` : "" - }`, - }, - { - title: t("jobs.fields.ins_co_nm"), - dataIndex: "ins_co_nm", - key: "ins_co_nm", - ellipsis: true, - filters: - (jobs && - jobs - .map((j) => j.ins_co_nm) - .filter(onlyUnique) - .map((s) => { - return { - text: s, - value: [s], - }; - })) || - [], - onFilter: (value, record) => value.includes(record.ins_co_nm), - responsive: ["md"], - }, - { - title: t("jobs.fields.clm_total"), - dataIndex: "clm_total", - key: "clm_total", - responsive: ["md"], - ellipsis: true, - - sorter: (a, b) => a.clm_total - b.clm_total, - sortOrder: - state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, - render: (text, record) => ( - {record.clm_total} - ), - }, - { - title: t("jobs.labels.estimator"), - dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", - ellipsis: true, - responsive: ["xl"], - filterSearch: true, - filters: - (jobs && - jobs - .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) - .filter(onlyUnique) - .map((s) => { - return { - text: s || "N/A", - value: [s], - }; - })) || - [], - onFilter: (value, record) => - value.includes( - `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() - ), - render: (text, record) => - `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), - }, - { - title: t("jobs.fields.comment"), - dataIndex: "comment", - key: "comment", - ellipsis: true, - responsive: ["md"], - }, - // { - // title: t("jobs.fields.owner_owing"), - // dataIndex: "owner_owing", - // key: "owner_owing", - // responsive: ["md"], - // render: (text, record) => ( - // {record.owner_owing} - // ), - // }, - ]; - - const scrollMapper = { - xs: true, - sm: true, - md: true, - lg: "100%", - xl: "100%", - xxl: "100%", - }; - - return ( - - - { - setSearchText(e.target.value); - }} - value={searchText} - enterButton - /> - - } - > -
{ - handleOnRowClick(record); - }, - selectedRowKeys: [selected], - type: "radio", - }} - onChange={handleTableChange} - onRow={(record, rowIndex) => { - return { - onClick: (event) => { - handleOnRowClick(record); - }, - }; - }} - /> - - ); + }; + }} + /> + + ); } export default connect(mapStateToProps, null)(JobsList); diff --git a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx index fee461df2..147f6ea23 100644 --- a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx +++ b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx @@ -16,11 +16,12 @@ import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { onlyUnique } from "../../utils/arrayHelper"; -import { alphaSort } from "../../utils/sorters"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, statusSort } from "../../utils/sorters"; +import useLocalStorage from "../../utils/useLocalStorage"; import AlertComponent from "../alert/alert.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import {pageLimit} from "../../utils/config"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -53,10 +54,8 @@ export function JobsReadyList({ bodyshop }) { nextFetchPolicy: "network-only", }); - const [state, setState] = useState({ - sortedInfo: {}, - filteredInfo: { text: "" }, - }); + const [state, setState] = useState({ sortedInfo: {} }); + const [filter, setFilter] = useLocalStorage("filter_jobs_ready", null); const { t } = useTranslation(); const history = useHistory(); @@ -105,7 +104,8 @@ export function JobsReadyList({ bodyshop }) { : []; const handleTableChange = (pagination, filters, sorter) => { - setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + setState({ ...state, sortedInfo: sorter }); + setFilter(filters); }; const handleOnRowClick = (record) => { @@ -129,7 +129,6 @@ export function JobsReadyList({ bodyshop }) { sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, - render: (text, record) => ( alphaSort(a.ownr_ln, b.ownr_ln), sortOrder: @@ -197,16 +195,15 @@ export function JobsReadyList({ bodyshop }) { ), }, - { title: t("jobs.fields.status"), dataIndex: "status", key: "status", ellipsis: true, - sorter: (a, b) => alphaSort(a.status, b.status), sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filteredValue: filter?.status || null, filters: (jobs && jobs @@ -217,11 +214,17 @@ export function JobsReadyList({ bodyshop }) { text: s || "No Status*", value: [s], }; - })) || + }) + .sort((a, b) => + statusSort( + a.text, + b.text, + bodyshop.md_ro_statuses.active_statuses + ) + )) || [], onFilter: (value, record) => value.includes(record.status), }, - { title: t("jobs.fields.vehicle"), dataIndex: "vehicle", @@ -274,6 +277,7 @@ export function JobsReadyList({ bodyshop }) { dataIndex: "ins_co_nm", key: "ins_co_nm", ellipsis: true, + filteredValue: filter?.ins_co_nm || null, filters: (jobs && jobs @@ -281,10 +285,11 @@ export function JobsReadyList({ bodyshop }) { .filter(onlyUnique) .map((s) => { return { - text: s, + text: s || "No Ins Co.*", value: [s], }; - })) || + }) + .sort((a, b) => alphaSort(a.text, b.text))) || [], onFilter: (value, record) => value.includes(record.ins_co_nm), responsive: ["md"], @@ -295,7 +300,6 @@ export function JobsReadyList({ bodyshop }) { key: "clm_total", responsive: ["md"], ellipsis: true, - sorter: (a, b) => a.clm_total - b.clm_total, sortOrder: state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, @@ -306,9 +310,10 @@ export function JobsReadyList({ bodyshop }) { { title: t("jobs.labels.estimator"), dataIndex: "jobs.labels.estimator", - key: "jobs.labels.estimator", + key: "estimator", ellipsis: true, responsive: ["xl"], + filteredValue: filter?.estimator || null, filterSearch: true, filters: (jobs && @@ -317,10 +322,11 @@ export function JobsReadyList({ bodyshop }) { .filter(onlyUnique) .map((s) => { return { - text: s || "N/A", + text: s || "No Estimator*", value: [s], }; - })) || + }) + .sort((a, b) => alphaSort(a.text, b.text))) || [], onFilter: (value, record) => value.includes( diff --git a/client/src/components/jobs-related-ros/jobs-related-ros.component.jsx b/client/src/components/jobs-related-ros/jobs-related-ros.component.jsx index 9f1a3b5b9..ffbd30e73 100644 --- a/client/src/components/jobs-related-ros/jobs-related-ros.component.jsx +++ b/client/src/components/jobs-related-ros/jobs-related-ros.component.jsx @@ -2,7 +2,7 @@ import { Space, Tag } from "antd"; import React from "react"; import { Link } from "react-router-dom"; -export default function JobsRelatedRos({ jobid, job }) { +export default function JobsRelatedRos({ jobid, job, disabled }) { if (!(job && job.vehicle && job.vehicle.jobs)) return null; return ( @@ -10,9 +10,15 @@ export default function JobsRelatedRos({ jobid, job }) { .filter((j) => j.id !== job.id) .map((j) => ( - {`${j.ro_number || "N/A"}${ - j.clm_no ? ` | ${j.clm_no}` : "" - }${j.status ? ` | ${j.status}` : ""}`} + {disabled ? ( + <>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${ + j.status ? ` | ${j.status}` : "" + }`} + ) : ( + {`${j.ro_number || "N/A"}${ + j.clm_no ? ` | ${j.clm_no}` : "" + }${j.status ? ` | ${j.status}` : ""}`} + )} ))} diff --git a/client/src/components/parts-queue-card/parts-queue-card.component.jsx b/client/src/components/parts-queue-card/parts-queue-card.component.jsx new file mode 100644 index 000000000..c703b7616 --- /dev/null +++ b/client/src/components/parts-queue-card/parts-queue-card.component.jsx @@ -0,0 +1,77 @@ +import { useQuery } from "@apollo/client"; +import { Card, Divider, Drawer, Grid } from "antd"; +import queryString from "query-string"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useHistory, useLocation } from "react-router-dom"; +import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries"; +import AlertComponent from "../alert/alert.component"; +import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component"; + +export default function PartsQueueDetailCard() { + const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) + .filter((screen) => !!screen[1]) + .slice(-1)[0]; + + const bpoints = { + xs: "100%", + sm: "100%", + md: "100%", + lg: "75%", + xl: "75%", + xxl: "60%", + }; + const drawerPercentage = selectedBreakpoint + ? bpoints[selectedBreakpoint[0]] + : "100%"; + + const searchParams = queryString.parse(useLocation().search); + const { selected } = searchParams; + const history = useHistory(); + const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, { + variables: { id: selected }, + skip: !selected, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + const { t } = useTranslation(); + const handleDrawerClose = () => { + delete searchParams.selected; + history.push({ + search: queryString.stringify({ + ...searchParams, + }), + }); + }; + + return ( + + {loading ? : null} + {error ? : null} + {data ? ( + + {data.jobs_by_pk.ro_number || t("general.labels.na")} + + } + > + + + + + ) : null} + + ); +} diff --git a/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx new file mode 100644 index 000000000..d680c1bef --- /dev/null +++ b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx @@ -0,0 +1,208 @@ +import { Card, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; +import { alphaSort } from "../../utils/sorters"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly, +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) { + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: {}, + }); + const { t } = useTranslation(); + + const columns = [ + { + title: "#", + dataIndex: "line_no", + key: "line_no", + sorter: (a, b) => a.line_no - b.line_no, + sortOrder: + state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order, + }, + { + title: t("joblines.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + onCell: (record) => ({ + className: record.manual_line && "job-line-manual", + style: { + ...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}), + }, + }), + sortOrder: + state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, + ellipsis: true, + }, + { + title: t("joblines.fields.oem_partno"), + dataIndex: "oem_partno", + key: "oem_partno", + sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno), + sortOrder: + state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order, + ellipsis: true, + render: (text, record) => + `${record.oem_partno || ""} ${ + record.alt_partno ? `(${record.alt_partno})` : "" + }`.trim(), + }, + { + title: t("joblines.fields.part_type"), + dataIndex: "part_type", + key: "part_type", + filteredValue: state.filteredInfo.part_type || null, + sorter: (a, b) => alphaSort(a.part_type, b.part_type), + sortOrder: + state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order, + filters: [ + { + text: t("jobs.labels.partsfilter"), + value: [ + "PAN", + "PAC", + "PAR", + "PAL", + "PAA", + "PAM", + "PAP", + "PAS", + "PASL", + "PAG", + ], + }, + { + text: t("joblines.fields.part_types.PAN"), + value: ["PAN"], + }, + { + text: t("joblines.fields.part_types.PAP"), + value: ["PAP"], + }, + { + text: t("joblines.fields.part_types.PAL"), + value: ["PAL"], + }, + { + text: t("joblines.fields.part_types.PAA"), + value: ["PAA"], + }, + { + text: t("joblines.fields.part_types.PAG"), + value: ["PAG"], + }, + { + text: t("joblines.fields.part_types.PAS"), + value: ["PAS"], + }, + { + text: t("joblines.fields.part_types.PASL"), + value: ["PASL"], + }, + { + text: t("joblines.fields.part_types.PAC"), + value: ["PAC"], + }, + { + text: t("joblines.fields.part_types.PAR"), + value: ["PAR"], + }, + { + text: t("joblines.fields.part_types.PAM"), + value: ["PAM"], + }, + ], + onFilter: (value, record) => value.includes(record.part_type), + render: (text, record) => + record.part_type + ? t(`joblines.fields.part_types.${record.part_type}`) + : null, + }, + { + title: t("joblines.fields.part_qty"), + dataIndex: "part_qty", + key: "part_qty", + }, + { + title: t("joblines.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + sorter: (a, b) => a.act_price - b.act_price, + sortOrder: + state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order, + ellipsis: true, + render: (text, record) => ( + + {record.db_ref === "900510" || record.db_ref === "900511" + ? record.prt_dsmk_m + : record.act_price} + + ), + }, + { + title: t("joblines.fields.location"), + dataIndex: "location", + key: "location", + }, + { + title: t("joblines.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => alphaSort(a.status, b.status), + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + filteredValue: state.filteredInfo.status || null, + filters: + (jobLines && + jobLines + .map((l) => l.status) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Status*", + value: [s], + }; + })) || + [], + onFilter: (value, record) => value.includes(record.status), + }, + ]; + + const handleTableChange = (pagination, filters, sorter) => { + setState((state) => ({ + ...state, + filteredInfo: filters, + sortedInfo: sorter, + })); + }; + + return ( + +
+ + ); +} +export default connect( + mapStateToProps, + mapDispatchToProps +)(PartsQueueJobLinesComponent); diff --git a/client/src/pages/parts-queue/parts-queue.page.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx similarity index 76% rename from client/src/pages/parts-queue/parts-queue.page.component.jsx rename to client/src/components/parts-queue-list/parts-queue.list.component.jsx index 62970d788..44d5368ce 100644 --- a/client/src/pages/parts-queue/parts-queue.page.component.jsx +++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx @@ -8,30 +8,28 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import AlertComponent from "../../components/alert/alert.component"; -import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component"; -import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component"; -import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component"; -import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component"; import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter"; +import { onlyUnique } from "../../utils/arrayHelper"; +import { pageLimit } from "../../utils/config"; import { alphaSort, dateSort } from "../../utils/sorters"; import useLocalStorage from "../../utils/useLocalStorage"; -import {pageLimit} from "../../utils/config"; +import AlertComponent from "../alert/alert.component"; +import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component"; +import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; +import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); -export function PartsQueuePageComponent({ bodyshop }) { +export function PartsQueueListComponent({ bodyshop }) { const searchParams = queryString.parse(useLocation().search); - const { - //page, - sortcolumn, - sortorder, - statusFilters, - } = searchParams; + const { selected, sortcolumn, sortorder, statusFilters } = searchParams; const history = useHistory(); const [filter, setFilter] = useLocalStorage("filter_parts_queue", null); @@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) { fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - // offset: page ? (page - 1) * 25 : 0, - // limit: 25, statuses: (statusFilters && JSON.parse(statusFilters)) || bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], - order: [ - { - [sortcolumn || "ro_number"]: sortorder - ? sortorder === "descend" - ? "desc" - : "asc" - : "desc", - }, - ], }, }); @@ -107,6 +94,19 @@ export function PartsQueuePageComponent({ bodyshop }) { history.push({ search: queryString.stringify(searchParams) }); }; + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + history.push({ + search: queryString.stringify({ + ...searchParams, + selected: record.id, + }), + }); + } + } + }; + const columns = [ { title: t("jobs.fields.ro_number"), @@ -125,7 +125,8 @@ export function PartsQueuePageComponent({ bodyshop }) { title: t("jobs.fields.owner"), dataIndex: "ownr_ln", key: "ownr_ln", - sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sorter: (a, b) => + alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)), sortOrder: sortcolumn === "ownr_ln" && sortorder, render: (text, record) => { return record.ownerid ? ( @@ -139,6 +140,56 @@ export function PartsQueuePageComponent({ bodyshop }) { ); }, }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${ + a.v_model_desc || "" + }`, + `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}` + ), + sortOrder: sortcolumn === "vehicle" && sortorder, + render: (text, record) => { + return record.vehicleid ? ( + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("jobs.fields.ins_co_nm_short"), + dataIndex: "ins_co_nm", + key: "ins_co_nm", + ellipsis: true, + sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm), + sortOrder: sortcolumn === "ins_co_nm" && sortorder, + filteredValue: filter?.ins_co_nm || null, + filters: + (jobs && + jobs + .map((j) => j.ins_co_nm) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "No Ins. Co.*", + value: [s], + }; + }) + .sort((a, b) => alphaSort(a.text, b.text))) || + [], + onFilter: (value, record) => value.includes(record.ins_co_nm), + }, { title: t("jobs.fields.status"), dataIndex: "status", @@ -170,23 +221,16 @@ export function PartsQueuePageComponent({ bodyshop }) { ), }, { - title: t("jobs.fields.vehicle"), - dataIndex: "vehicle", - key: "vehicle", + title: t("jobs.fields.scheduled_completion"), + dataIndex: "scheduled_completion", + key: "scheduled_completion", ellipsis: true, - render: (text, record) => { - return record.vehicleid ? ( - - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - - ) : ( - {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ - record.v_model_desc || "" - }`} - ); - }, + sorter: (a, b) => + dateSort(a.scheduled_completion, b.scheduled_completion), + sortOrder: sortcolumn === "scheduled_completion" && sortorder, + render: (text, record) => ( + {record.scheduled_completion} + ), }, // { // title: t("vehicles.fields.plate_no"), @@ -198,14 +242,6 @@ export function PartsQueuePageComponent({ bodyshop }) { // return record.plate_no ? record.plate_no : ""; // }, // }, - { - title: t("jobs.fields.clm_no"), - dataIndex: "clm_no", - key: "clm_no", - ellipsis: true, - sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), - sortOrder: sortcolumn === "clm_no" && sortorder, - }, // { // title: t("jobs.fields.clm_total"), // dataIndex: "clm_total", @@ -307,9 +343,16 @@ export function PartsQueuePageComponent({ bodyshop }) { style={{ height: "100%" }} scroll={{ x: true }} onChange={handleTableChange} + rowSelection={{ + onSelect: (record) => { + handleOnRowClick(record); + }, + selectedRowKeys: [selected], + type: "radio", + }} /> ); } -export default connect(mapStateToProps, null)(PartsQueuePageComponent); +export default connect(mapStateToProps, null)(PartsQueueListComponent); diff --git a/client/src/components/production-board-kanban/production-board-kanban.component.jsx b/client/src/components/production-board-kanban/production-board-kanban.component.jsx index b1061f973..9421de918 100644 --- a/client/src/components/production-board-kanban/production-board-kanban.component.jsx +++ b/client/src/components/production-board-kanban/production-board-kanban.component.jsx @@ -115,7 +115,7 @@ export function ProductionBoardKanbanComponent({ // console.log("==> New Card is somewhere in the middle"); movedCardNewKanbanParent = newChildCard.kanbanparent; } else { - throw new Error("==> !!!!!!Couldn't find a parent.!!!! <=="); + console.log("==> !!!!!!Couldn't find a parent.!!!! <=="); } const newChildCardNewParent = newChildCard ? card.id : null; const update = await client.mutate({ @@ -153,7 +153,7 @@ export function ProductionBoardKanbanComponent({ 0 ) .toFixed(1); - const totalLAB = data + const totalLAB = data .reduce( (acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0 diff --git a/client/src/components/production-list-columns/production-list-columns.add.component.jsx b/client/src/components/production-list-columns/production-list-columns.add.component.jsx index 82f884673..c79e9db7f 100644 --- a/client/src/components/production-list-columns/production-list-columns.add.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.add.component.jsx @@ -1,7 +1,7 @@ -import React from "react"; import { Button, Dropdown, Menu } from "antd"; -import dataSource from "./production-list-columns.data"; +import React from "react"; import { useTranslation } from "react-i18next"; +import dataSource from "./production-list-columns.data"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -24,6 +24,7 @@ export function ProductionColumnsComponent({ columnState, technician, bodyshop, + data, tableState, }) { const [columns, setColumns] = columnState; @@ -36,6 +37,7 @@ export function ProductionColumnsComponent({ bodyshop, technician, state: tableState, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).filter((i) => i.key === e.key), ]); @@ -46,6 +48,7 @@ export function ProductionColumnsComponent({ technician, state: tableState, activeStatuses: bodyshop.md_ro_statuses.active_statuses, + data: data, }); const menu = ( { +const r = ({ technician, state, activeStatuses, data, bodyshop }) => { return [ { title: i18n.t("jobs.actions.viewdetail"), @@ -75,7 +76,14 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { dataIndex: "ownr", key: "ownr", ellipsis: true, - render: (text, record) => , + render: (text, record) => + technician ? ( + + ) : ( + + + + ), sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), sortOrder: state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order, @@ -92,13 +100,18 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { ), sortOrder: state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order, - render: (text, record) => ( - {`${ - record.v_model_yr || "" - } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ - record.v_color || "" - } ${record.plate_no || ""}`} - ), + render: (text, record) => + technician ? ( + <>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + } ${record.v_color || ""} ${record.plate_no || ""}`} + ) : ( + {`${ + record.v_model_yr || "" + } ${record.v_make_desc || ""} ${record.v_model_desc || ""} ${ + record.v_color || "" + } ${record.plate_no || ""}`} + ), }, { title: i18n.t("jobs.fields.actual_in"), @@ -536,6 +549,36 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => { ), }, + { + title: i18n.t("jobs.labels.estimator"), + dataIndex: "estimator", + key: "estimator", + sorter: (a, b) => + alphaSort( + `${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(), + `${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim() + ), + sortOrder: + state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order, + filters: + (data && + data + .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "N/A", + value: [s], + }; + })) || + [], + onFilter: (value, record) => + value.includes( + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() + ), + render: (text, record) => + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), + }, //Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client. // { diff --git a/client/src/components/production-list-detail/production-list-detail.component.jsx b/client/src/components/production-list-detail/production-list-detail.component.jsx index 170b31b46..34265612b 100644 --- a/client/src/components/production-list-detail/production-list-detail.component.jsx +++ b/client/src/components/production-list-detail/production-list-detail.component.jsx @@ -13,12 +13,14 @@ import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter } from "../../utils/DateFormatter"; +import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import AlertComponent from "../alert/alert.component"; import StartChatButton from "../chat-open-button/chat-open-button.component"; import JobAtChange from "../job-at-change/job-at-change.component"; import JobDetailCardsDocumentsComponent from "../job-detail-cards/job-detail-cards.documents.component"; import JobDetailCardsNotesComponent from "../job-detail-cards/job-detail-cards.notes.component"; import JobDetailCardsPartsComponent from "../job-detail-cards/job-detail-cards.parts.component"; +import CardTemplate from "../job-detail-cards/job-detail-cards.template.component"; import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container"; import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; @@ -103,7 +105,13 @@ export function ProductionListDetail({ {error && } {!loading && data && (
- + + + +
{theJob.ro_number || ""} @@ -111,7 +119,7 @@ export function ProductionListDetail({ {data.jobs_by_pk.alt_transport || ""} - + @@ -121,15 +129,30 @@ export function ProductionListDetail({ {theJob.ins_co_nm || ""} - - - + + + {!technician ? ( + <> + + + + ) : ( + <> + + {data.jobs_by_pk.ownr_ph1} + + + {data.jobs_by_pk.ownr_ph2} + + + )} + {`${theJob.v_model_yr || ""} ${theJob.v_color || ""} ${ @@ -146,21 +169,24 @@ export function ProductionListDetail({ {theJob.scheduled_completion} - +
- +
{!bodyshop.uselocalmediaserver && ( - + <> +
+ + )}
)} diff --git a/client/src/components/production-list-table/production-list-table-view-select.component.jsx b/client/src/components/production-list-table/production-list-table-view-select.component.jsx index 044c75994..82086ac96 100644 --- a/client/src/components/production-list-table/production-list-table-view-select.component.jsx +++ b/client/src/components/production-list-table/production-list-table-view-select.component.jsx @@ -24,6 +24,7 @@ export function ProductionListTable({ technician, currentUser, state, + data, setColumns, setState, }) { @@ -41,6 +42,7 @@ export function ProductionListTable({ bodyshop, technician, state, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), width: k.width, @@ -95,6 +97,7 @@ export function ProductionListTable({ ...ProductionListColumns({ technician, state, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), width: k.width, diff --git a/client/src/components/production-list-table/production-list-table.component.jsx b/client/src/components/production-list-table/production-list-table.component.jsx index 3ea3391d3..5257cea9d 100644 --- a/client/src/components/production-list-table/production-list-table.component.jsx +++ b/client/src/components/production-list-table/production-list-table.component.jsx @@ -10,7 +10,7 @@ import { Statistic, Table, } from "antd"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import ReactDragListView from "react-drag-listview"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -79,6 +79,7 @@ export function ProductionListTable({ bodyshop, technician, state, + data: data, activeStatuses: bodyshop.md_ro_statuses.active_statuses, }).find((e) => e.key === k.key), width: k.width ?? 100, @@ -87,6 +88,33 @@ export function ProductionListTable({ [] ); + useEffect(() => { + const newColumns = + (state && + matchingColumnConfig && + matchingColumnConfig.columns.columnKeys.map((k) => { + return { + ...ProductionListColumns({ + bodyshop, + technician, + state, + data: data, + activeStatuses: bodyshop.md_ro_statuses.active_statuses, + }).find((e) => e.key === k.key), + width: k.width ?? 100, + }; + })) || + []; + setColumns(newColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + //state, + matchingColumnConfig, + bodyshop, + technician, + data, + ]); //State removed from dependency array as it causes race condition when removing columns from table view and is not needed. + const handleTableChange = (pagination, filters, sorter) => { setState({ ...state, @@ -104,7 +132,8 @@ export function ProductionListTable({ const removeColumn = (e) => { const { key } = e; - setColumns(columns.filter((i) => i.key !== key)); + const newColumns = columns.filter((i) => i.key !== key); + setColumns(newColumns); }; const handleResize = @@ -227,6 +256,7 @@ export function ProductionListTable({ + {() => { + const key = form.getFieldValue("key"); + return ; + }} + + ); +} + +/** + * Filters Section + * @param filters + * @param form + * @param bodyshop + * @returns {JSX.Element} + * @constructor + */ +function FiltersSection({filters, form, bodyshop}) { + const {t} = useTranslation(); + + return ( + + + {(fields, {add, remove, move}) => { + return ( +
+ {fields.map((field, index) => ( + + +
+ + trigger.parentNode} + options={getWhereOperatorsByType(type)}/> + + } + } + + + + + { + () => { + const name = form.getFieldValue(['filters', field.name, "field"]); + const type = filters.find(f => f.name === name)?.type; + const reflector = filters.find(f => f.name === name)?.reflector; + + return + { + (() => { + const generateReflections = (reflector) => { + if (!reflector) return []; + + const {name} = reflector; + const path = name?.split('.'); + const upperPath = path?.[0]; + const finalPath = path?.slice(1).join('.'); + + return generateInternalReflections({ + bodyshop, + upperPath, + finalPath + }); + }; + + const reflections = reflector ? generateReflections(reflector) : []; + const fieldPath = [[field.name, "value"]]; + + if (reflections.length > 0) { + return ( + form.setFieldValue(fieldPath, e.target.value)}/> + ); + })() + } + + } + } + + + + + { + remove(field.name); + }} + /> + + + + ))} + + + + + ); + }} + + + ); +} + +/** + * Sorters Section + * @param sorters + * @param form + * @returns {JSX.Element} + * @constructor + */ +function SortersSection({sorters, form}) { + const {t} = useTranslation(); + return ( + + + {(fields, {add, remove, move}) => { + return ( +
+ Sorters + {fields.map((field, index) => ( + + +
+ + trigger.parentNode} + /> + + + + + { + remove(field.name); + }} + /> + + + + ))} + + + + + ); + }} + + + ); +} + +/** + * Render Filters + * @param templateId + * @param form + * @param bodyshop + * @returns {JSX.Element|null} + * @constructor + */ +function RenderFilters({templateId, form, bodyshop}) { + const [state, setState] = useState(null); + const [visible, setVisible] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const {t} = useTranslation(); + + const fetch = useCallback(async () => { + // Reset all the filters and Sorters. + form.resetFields(['filters']); + form.resetFields(['sorters']); + form.resetFields(['defaultSorters']); + + setIsLoading(true); + + const data = await fetchFilterData({name: templateId}); + + // We have Success + if (data?.success) { + if (data?.data?.sorters && data?.data?.sorters.length > 0) { + const defaultSorters = data?.data?.sorters.filter((sorter) => sorter.hasOwnProperty('default')).map((sorter) => { + return { + field: sorter.name, + direction: sorter.default.direction + }; + }).sort((a, b) => a.default.order - b.default.order); + + form.setFieldValue('defaultSorters', JSON.stringify(defaultSorters)); + } + // Set the state + setState(data.data); + } + // Something went wrong fetching filter data + else { + setState(null); + } + setIsLoading(false); + }, [templateId, form]); + + useEffect(() => { + if (templateId) { + fetch(); + + } + }, [templateId, fetch]); + + const filters = useMemo(() => state?.filters || [], [state]); + const sorters = useMemo(() => state?.sorters || [], [state]); + + if (!templateId) return null; + if (isLoading) return ; + if (!state) return null; + + return ( +
+ setVisible(e.target.checked)} + children={t('reportcenter.labels.advanced_filters')} + /> + {visible && ( +
+ {filters.length > 0 && ( + + )} + {sorters.length > 0 && ( + + )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/client/src/components/report-center-modal/report-center-modal-utils.js b/client/src/components/report-center-modal/report-center-modal-utils.js new file mode 100644 index 000000000..2f9fc5e87 --- /dev/null +++ b/client/src/components/report-center-modal/report-center-modal-utils.js @@ -0,0 +1,121 @@ +import {uniqBy} from "lodash"; + +/** + * Get value from path + * @param obj + * @param path + * @returns {*} + */ +const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj); + +/** + * Valid internal reflections + * Note: This is intended for future functionality + * @type {{special: string[], bodyshop: [{name: string, type: string}]}} + */ +const VALID_INTERNAL_REFLECTIONS = { + bodyshop: [ + { + name: 'md_ro_statuses.statuses', + type: 'kv-to-v' + } + ], +}; + +/** + * Generate options + * @param bodyshop + * @param path + * @param labelPath + * @param valuePath + * @returns {{label: *, value: *}[]} + */ +const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => { + const options = getValueFromPath(bodyshop, path); + return uniqBy(Object.values(options).map((value) => ({ + label: value[labelPath], + value: value[valuePath], + })), 'value'); +} + +/** + * Generate special reflections + * @param bodyshop + * @param finalPath + * @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]} + */ +const generateSpecialReflections = (bodyshop, finalPath) => { + switch (finalPath) { + case 'cost_centers': + return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name'); + // Special case because Categories is an Array, not an Object. + case 'categories': + const catOptions = getValueFromPath(bodyshop, 'md_categories'); + return uniqBy(catOptions.map((value) => ({ + label: value, + value: value, + })), 'value'); + case 'insurance_companies': + return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name'); + case 'employee_teams': + return generateOptionsFromObject(bodyshop, 'employee_teams', 'name', 'id'); + // Special case because Employees uses a concatenation of first_name and last_name + case 'employees': + const employeesOptions = getValueFromPath(bodyshop, 'employees'); + return uniqBy(Object.values(employeesOptions).map((value) => ({ + label: `${value.first_name} ${value.last_name}`, + value: value.id, + })), 'value'); + case 'last_names': + return generateOptionsFromObject(bodyshop, 'employees', 'last_name', 'last_name'); + case 'first_names': + return generateOptionsFromObject(bodyshop, 'employees', 'first_name', 'first_name'); + case 'job_statuses': + const statusOptions = getValueFromPath(bodyshop, 'md_ro_statuses.statuses'); + return Object.values(statusOptions).map((value) => ({ + label: value, + value + })); + default: + console.error('Invalid Special reflection provided by Report Filters'); + return []; + } +} + +/** + * Generate bodyshop reflections + * @param bodyshop + * @param finalPath + * @returns {{label: *, value: *}[]|*[]} + */ +const generateBodyshopReflections = (bodyshop, finalPath) => { + const options = getValueFromPath(bodyshop, finalPath); + const reflectionRenderer = VALID_INTERNAL_REFLECTIONS.bodyshop.find(reflection => reflection.name === finalPath); + if (reflectionRenderer?.type === 'kv-to-v') { + return Object.values(options).map((value) => ({ + label: value, + value + })); + } + return []; +} + +/** + * Generate internal reflections based on the path and bodyshop + * @param bodyshop + * @param upperPath + * @param finalPath + * @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]} + */ +const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => { + switch (upperPath) { + case 'special': + return generateSpecialReflections(bodyshop, finalPath); + case 'bodyshop': + return generateBodyshopReflections(bodyshop, finalPath); + default: + return []; + } +}; + +export {generateInternalReflections,} \ No newline at end of file diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index 913d27282..1a451f134 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -1,305 +1,321 @@ -import { useLazyQuery } from "@apollo/client"; -import { - Button, - Card, - Col, - DatePicker, - Form, - Input, - Radio, - Row, - Typography, -} from "antd"; +import {useLazyQuery} from "@apollo/client"; +import {Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography,} from "antd"; import _ from "lodash"; import moment from "moment"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { QUERY_ACTIVE_EMPLOYEES } from "../../graphql/employees.queries"; -import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; -import { selectReportCenter } from "../../redux/modals/modals.selectors"; -import DatePIckerRanges from "../../utils/DatePickerRanges"; -import { GenerateDocument } from "../../utils/RenderTemplate"; -import { TemplateList } from "../../utils/TemplateConstants"; +import React, {useState} from "react"; +import {useTranslation} from "react-i18next"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {QUERY_ACTIVE_EMPLOYEES} from "../../graphql/employees.queries"; +import {QUERY_ALL_VENDORS} from "../../graphql/vendors.queries"; +import {selectReportCenter} from "../../redux/modals/modals.selectors"; +import DatePickerRanges from "../../utils/DatePickerRanges"; +import {GenerateDocument} from "../../utils/RenderTemplate"; +import {TemplateList} from "../../utils/TemplateConstants"; import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import "./report-center-modal.styles.scss"; +import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; +import {selectBodyshop} from "../../redux/user/user.selectors"; + const mapStateToProps = createStructuredSelector({ - reportCenterModal: selectReportCenter, + reportCenterModal: selectReportCenter, + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps, + mapDispatchToProps )(ReportCenterModalComponent); -export function ReportCenterModalComponent({ reportCenterModal }) { +export function ReportCenterModalComponent({reportCenterModal, bodyshop}) { const [form] = Form.useForm(); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(false); - const { t } = useTranslation(); + const {t} = useTranslation(); const Templates = TemplateList("report_center"); const ReportsList = Object.keys(Templates).map((key) => { return Templates[key]; }); - const { visible } = reportCenterModal; + const {open} = reportCenterModal; - const [callVendorQuery, { data: vendorData, called: vendorCalled }] = - useLazyQuery(QUERY_ALL_VENDORS, { - skip: !( - visible && - Templates[form.getFieldValue("key")] && - Templates[form.getFieldValue("key")].idtype - ), - }); + const [callVendorQuery, {data: vendorData, called: vendorCalled}] = + useLazyQuery(QUERY_ALL_VENDORS, { + skip: !( + open && + Templates[form.getFieldValue("key")] && + Templates[form.getFieldValue("key")].idtype + ), + }); - const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] = - useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { - skip: !( - visible && - Templates[form.getFieldValue("key")] && - Templates[form.getFieldValue("key")].idtype - ), - }); + const [callEmployeeQuery, {data: employeeData, called: employeeCalled}] = + useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { + skip: !( + open && + Templates[form.getFieldValue("key")] && + Templates[form.getFieldValue("key")].idtype + ), + }); const handleFinish = async (values) => { setLoading(true); - const start = values.dates[0]; - const end = values.dates[1]; + const start = values.dates ? values.dates[0] : null; + const end = values.dates ? values.dates[1] : null; const { id } = values; - await GenerateDocument( - { - name: values.key, - variables: { - ...(start - ? { start: moment(start).startOf("day").format("YYYY-MM-DD") } - : {}), - ...(end - ? { end: moment(end).endOf("day").format("YYYY-MM-DD") } - : {}), - ...(start ? { starttz: moment(start).startOf("day") } : {}), - ...(end ? { endtz: moment(end).endOf("day") } : {}), + const templateConfig = { + name: values.key, + variables: { + ...(start + ? {start: moment(start).startOf("day").format("YYYY-MM-DD")} + : {}), + ...(end ? {end: moment(end).endOf("day").format("YYYY-MM-DD")} : {}), + ...(start ? {starttz: moment(start).startOf("day")} : {}), + ...(end ? {endtz: moment(end).endOf("day")} : {}), - ...(id ? { id: id } : {}), + ...(id ? {id: id} : {}), + }, + filters: values.filters, + sorters: values.sorters, + }; + + if (_.isString(values.defaultSorters) && !_.isEmpty(values.defaultSorters)) { + templateConfig.defaultSorters = JSON.parse(values.defaultSorters); + } + + await GenerateDocument( + templateConfig, + { + to: values.to, + subject: Templates[values.key]?.subject, }, - }, - { - to: values.to, - subject: Templates[values.key]?.subject, - }, - values.sendbyexcel === "excel" - ? "x" - : values.sendby === "email" - ? "e" - : "p", - id + values.sendbyexcel === "excel" + ? "x" + : values.sendby === "email" + ? "e" + : "p", + id ); setLoading(false); }; const FilteredReportsList = - search !== "" - ? ReportsList.filter((r) => - r.title.toLowerCase().includes(search.toLowerCase()) - ) - : ReportsList; + search !== "" + ? ReportsList.filter((r) => + r.title.toLowerCase().includes(search.toLowerCase()) + ) + : ReportsList; //Group it, create cards, and then filter out. const grouped = _.groupBy(FilteredReportsList, "group"); return ( -
-
- setSearch(e.target.value)} - value={search} - /> - + - - {/* {Object.keys(Templates).map((key) => ( + setSearch(e.target.value)} + value={search} + /> +
- - - {t(`reportcenter.labels.groups.${key}`)} - -
    - {grouped[key].map((item) => ( -
  • - - {item.title} - -
  • - ))} -
-
- - ))} - - - - - {() => { - const key = form.getFieldValue("key"); - if (!key) return null; - //Kind of Id - const rangeFilter = Templates[key] && Templates[key].rangeFilter; - if (!rangeFilter) return null; - return ( -
- {t("reportcenter.labels.filterson", { - object: rangeFilter.object, - field: rangeFilter.field, - })} -
- ); - }} -
- - {() => { - const key = form.getFieldValue("key"); - const currentId = form.getFieldValue("id"); - if (!key) return null; - //Kind of Id - const idtype = Templates[key] && Templates[key].idtype; - if (!idtype && currentId) { - form.setFieldsValue({ id: null }); - return null; - } - if (!vendorCalled && idtype === "vendor") callVendorQuery(); - if (!employeeCalled && idtype === "employee") callEmployeeQuery(); - if (idtype === "vendor") + + {Object.keys(grouped).map((key) => ( + + + + {t(`reportcenter.labels.groups.${key}`)} + +
    + {grouped[key].map((item) => ( +
  • + + {item.title} + +
  • + ))} +
+
+ + ))} + + + + + {() => { + const key = form.getFieldValue("key"); + if (!key) return null; + //Kind of Id + const rangeFilter = Templates[key] && Templates[key].rangeFilter; + if (!rangeFilter) return null; return ( - - - +
+ {t("reportcenter.labels.filterson", { + object: rangeFilter.object, + field: rangeFilter.field, + })} +
); - if (idtype === "employee") - return ( - - - - ); - else return null; - }} -
- - - - - {() => { - const key = form.getFieldValue("key"); - //Kind of Id - const reporttype = Templates[key] && Templates[key].reporttype; + }} + + + + {() => { + const key = form.getFieldValue("key"); + const currentId = form.getFieldValue("id"); + if (!key) return null; + //Kind of Id + const idtype = Templates[key] && Templates[key].idtype; + if (!idtype && currentId) { + form.setFieldsValue({id: null}); + return null; + } + if (!vendorCalled && idtype === "vendor") callVendorQuery(); + if (!employeeCalled && idtype === "employee") callEmployeeQuery(); + if (idtype === "vendor") + return ( + + + + ); + if (idtype === "employee") + return ( + + + + ); + else return null; + }} + + + {() => { + const key = form.getFieldValue("key"); + const datedisable = Templates[key] && Templates[key].datedisable; - if (reporttype === "excel") - return ( - - - {t("general.labels.excel")} - - - ); - if (reporttype !== "excel") - return ( - - - {t("general.labels.email")} - {t("general.labels.print")} - - - ); - }} - + // TODO: MERGE NOTE, Ranges turns to presets in DatePicker.RangePicker -
- -
- - + if (datedisable !== true) { + return ( + + + + ); + } else return null; + }} + + + {() => { + const key = form.getFieldValue("key"); + //Kind of Id + const reporttype = Templates[key] && Templates[key].reporttype; + + if (reporttype === "excel") + return ( + + + {t("general.labels.excel")} + + + ); + if (reporttype !== "excel") + return ( + + + {t("general.labels.email")} + {t("general.labels.print")} + + + ); + }} + + +
+ +
+ + ); } + diff --git a/client/src/components/report-center-modal/report-center-modal.container.jsx b/client/src/components/report-center-modal/report-center-modal.container.jsx index f0d361785..84fe65560 100644 --- a/client/src/components/report-center-modal/report-center-modal.container.jsx +++ b/client/src/components/report-center-modal/report-center-modal.container.jsx @@ -5,6 +5,7 @@ import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectReportCenter } from "../../redux/modals/modals.selectors"; +import RbacWrapperComponent from "../rbac-wrapper/rbac-wrapper.component"; import ReportCenterModalComponent from "./report-center-modal.component"; const mapStateToProps = createStructuredSelector({ @@ -33,7 +34,9 @@ export function ReportCenterModalContainer({ destroyOnClose width="80%" > - + + + ); } diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx index 19c360a21..19d6d4a37 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx @@ -216,6 +216,7 @@ export function ScheduleJobModalContainer({ okButtonProps={{ loading: loading, }} + closable={false} >
{ setLoading(true); + values.date = moment(values.date).format("YYYY-MM-DD"); const result = await updateScoreboardentry({ variables: { sbId: entry.id, sbInput: values }, }); @@ -77,13 +87,14 @@ export default function ScoreboardEntryEdit({ entry }) { > - - - + + + + ); diff --git a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx index 6b1c13ca3..67267fb3f 100644 --- a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx +++ b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx @@ -1,3 +1,4 @@ +import { SyncOutlined } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; import { Button, Card, Input, Modal, Space, Table, Typography } from "antd"; import React, { useState } from "react"; @@ -5,12 +6,14 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { QUERY_SCOREBOARD_PAGINATED } from "../../graphql/scoreboard.queries"; import { DateFormatter } from "../../utils/DateFormatter"; +import { pageLimit } from "../../utils/config"; +import { alphaSort, dateSort } from "../../utils/sorters"; import AlertComponent from "../alert/alert.component"; -import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import OwnerNameDisplay, { + OwnerNameDisplayFunction, +} from "../owner-name-display/owner-name-display.component"; import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component"; import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component"; -import { SyncOutlined } from "@ant-design/icons"; -import {pageLimit} from "../../utils/config"; export default function ScoreboardJobsList({ scoreBoardlist }) { const { t } = useTranslation(); const [state, setState] = useState({ @@ -44,6 +47,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { title: t("jobs.fields.ro_number"), dataIndex: "ro_number", key: "ro_number", + sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number), render: (text, record) => ( {record.job.ro_number || t("general.labels.na")} @@ -55,7 +59,11 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { dataIndex: "owner", key: "owner", ellipsis: true, - + sorter: (a, b) => + alphaSort( + OwnerNameDisplayFunction(a.job), + OwnerNameDisplayFunction(b.job) + ), render: (text, record) => , }, { @@ -63,6 +71,15 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { dataIndex: "vehicle", key: "vehicle", ellipsis: true, + sorter: (a, b) => + alphaSort( + `${a.job.v_model_yr || ""} ${a.job.v_make_desc || ""} ${ + a.job.v_model_desc || "" + }`, + `${b.job.v_model_yr || ""} ${b.job.v_make_desc || ""} ${ + b.job.v_model_desc || "" + }` + ), render: (text, record) => ( {`${record.job.v_model_yr || ""} ${ record.job.v_make_desc || "" @@ -73,17 +90,20 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { title: t("scoreboard.fields.date"), dataIndex: "date", key: "date", + sorter: (a, b) => dateSort(a.date, b.date), render: (text, record) => {record.date}, }, - { - title: t("scoreboard.fields.painthrs"), - dataIndex: "painthrs", - key: "painthrs", - }, { title: t("scoreboard.fields.bodyhrs"), dataIndex: "bodyhrs", key: "bodyhrs", + sorter: (a, b) => Number(a.bodyhrs) - Number(b.bodyhrs), + }, + { + title: t("scoreboard.fields.painthrs"), + dataIndex: "painthrs", + key: "painthrs", + sorter: (a, b) => Number(a.painthrs) - Number(b.painthrs), }, { title: t("general.labels.actions"), @@ -104,8 +124,9 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { visible={state.visible} destroyOnClose width="80%" + closable={false} cancelButtonProps={{ style: { display: "none" } }} - onCancel={() => + onOk={() => setState((state) => ({ ...state, visible: false, diff --git a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx index 0117279d5..af4f28695 100644 --- a/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets-stats/scoreboard-timetickets.component.jsx @@ -29,7 +29,7 @@ export default connect( export function ScoreboardTimeTicketsStats({ bodyshop }) { const { t } = useTranslation(); - const startDate = moment().startOf("month") + const startDate = moment().startOf("month"); const endDate = moment().endOf("month"); const fixedPeriods = useMemo(() => { @@ -84,6 +84,8 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", @@ -340,11 +342,21 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) { larData.push({ ...r, ...lar }); }); + const jobData = {}; + data.jobs.forEach((job) => { + job.tthrs = job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0); + }); + jobData.tthrs = data.jobs + .reduce((acc, val) => acc + val.tthrs, 0) + .toFixed(1); + jobData.count = data.jobs.length.toFixed(0); + return { fixed: ret, combinedData: combinedData, labData: labData, larData: larData, + jobData: jobData, }; }, [fixedPeriods, data, bodyshop]); @@ -356,7 +368,10 @@ export function ScoreboardTimeTicketsStats({ bodyshop }) {
- + {/* This Month */} - + @@ -482,7 +482,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Last Month */} - + @@ -556,7 +556,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { {/* Efficiency Over Period */} - + + + + + + + + + + + + {t("scoreboard.labels.totalhrs")} + + } + value={jobData.tthrs} + valueStyle={{ + fontSize: statisticSize, + fontWeight: statisticWeight, + }} + /> + + + + {/* Disclaimer */} diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx index 0fc84af6d..9fdb384d1 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx @@ -65,6 +65,8 @@ export default function ScoreboardTimeTickets() { end: endDate.format("YYYY-MM-DD"), fixedStart: fixedPeriods.start.format("YYYY-MM-DD"), fixedEnd: fixedPeriods.end.format("YYYY-MM-DD"), + jobStart: startDate, + jobEnd: endDate, }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", diff --git a/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx b/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx index 4ed0c6033..9818bf896 100644 --- a/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx +++ b/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx @@ -1,5 +1,5 @@ -import React from "react"; import { Form } from "antd"; +import React from "react"; import ConfigFormComponents from "../config-form-components/config-form-components.component"; export default function ShopCsiConfigForm({ selectedCsi }) { @@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) { return (
- The Config Form {readOnly} + {readOnly} {selectedCsi && (
; return (
- The Config Form
- + + diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx index 416e875f9..dd540745f 100644 --- a/client/src/components/shop-employees/shop-employees-list.component.jsx +++ b/client/src/components/shop-employees/shop-employees-list.component.jsx @@ -1,14 +1,20 @@ import { Button, Table } from "antd"; import queryString from "query-string"; -import React from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useHistory, useLocation } from "react-router-dom"; +import { alphaSort } from "../../utils/sorters"; export default function ShopEmployeesListComponent({ loading, employees }) { const { t } = useTranslation(); const history = useHistory(); const search = queryString.parse(useLocation().search); + const [state, setState] = useState({ + sortedInfo: {}, + filteredInfo: { text: "" }, + }); + const handleOnRowClick = (record) => { if (record) { search.employeeId = record.id; @@ -18,32 +24,82 @@ export default function ShopEmployeesListComponent({ loading, employees }) { history.push({ search: queryString.stringify(search) }); } }; + + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + const columns = [ { title: t("employees.fields.employee_number"), dataIndex: "employee_number", key: "employee_number", + sorter: (a, b) => alphaSort(a.employee_number, b.employee_number), + sortOrder: + state.sortedInfo.columnKey === "employee_number" && + state.sortedInfo.order, }, { - title: t("employees.fields.first_name"), - dataIndex: "first_name", - key: "first_name", + title: t("employees.labels.name"), + dataIndex: "employee_name", + key: "employee_name", + sorter: (a, b) => + alphaSort( + `${a.first_name || ""} ${a.last_name || ""}`.trim(), + `${b.first_name || ""} ${b.last_name || ""}`.trim() + ), + sortOrder: + state.sortedInfo.columnKey === "employee_name" && + state.sortedInfo.order, + render: (text, record) => + `${record.first_name || ""} ${record.last_name || ""}`.trim(), }, - { - title: t("employees.fields.last_name"), - dataIndex: "last_name", - key: "last_name", - }, - { title: t("employees.labels.rate_type"), dataIndex: "rate_type", key: "rate_type", + sorter: (a, b) => Number(a.flat_rate) - Number(b.flat_rate), + sortOrder: + state.sortedInfo.columnKey === "rate_type" && state.sortedInfo.order, + filters: [ + { + text: t("employees.labels.flat_rate"), + value: true, + }, + { + text: t("employees.labels.straight_time"), + value: false, + }, + ], + onFilter: (value, record) => value === record.flate_rate, render: (text, record) => record.flat_rate ? t("employees.labels.flat_rate") : t("employees.labels.straight_time"), }, + { + title: t("employees.labels.status"), + dataIndex: "active", + key: "active", + sorter: (a, b) => Number(a.active) - Number(b.active), + sortOrder: + state.sortedInfo.columnKey === "active" && state.sortedInfo.order, + filters: [ + { + text: t("employees.labels.active"), + value: true, + }, + { + text: t("employees.labels.inactive"), + value: false, + }, + ], + onFilter: (value, record) => value === record.active, + render: (text, record) => + record.active + ? t("employees.labels.active") + : t("employees.labels.inactive"), + }, ]; return (
@@ -74,6 +130,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) { type: "radio", selectedRowKeys: [search.employeeId], }} + onChange={handleTableChange} onRow={(record, rowIndex) => { return { onClick: (event) => { diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx index fe4f80f31..e4152fb88 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -28,18 +28,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { return ( - - - + + + + + + + + + + + + @@ -173,26 +209,38 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { + + + @@ -208,30 +256,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - - - - - - - @@ -280,6 +292,18 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > + + + + + + + + + + + + + + + @@ -329,74 +401,14 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { - - - - - - - - - - - - - - - @@ -412,18 +424,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + - - - - - - @@ -556,18 +556,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) { > - - - + + + + + + + + + + + + + + + - - - - - - - - - {Simple_Inventory.treatment === "on" && ( <> ({ validator(rule, value) { - console.log( - bodyshop.tt_enforce_hours_for_tech_console - ); if (!bodyshop.tt_enforce_hours_for_tech_console) { return Promise.resolve(); } diff --git a/client/src/components/tech-login/tech-login.component.jsx b/client/src/components/tech-login/tech-login.component.jsx index 502b20cbf..c266803a5 100644 --- a/client/src/components/tech-login/tech-login.component.jsx +++ b/client/src/components/tech-login/tech-login.component.jsx @@ -1,7 +1,8 @@ import { Button, Form, Input } from "antd"; -import React from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; +import { Redirect } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { techLoginStart } from "../../redux/tech/tech.actions"; import { @@ -11,7 +12,6 @@ import { } from "../../redux/tech/tech.selectors"; import AlertComponent from "../alert/alert.component"; import "./tech-login.styles.scss"; -import { Redirect } from "react-router-dom"; const mapStateToProps = createStructuredSelector({ technician: selectTechnician, @@ -35,6 +35,10 @@ export function TechLogin({ techLoginStart(values); }; + useEffect(() => { + document.title = t("titles.techconsole"); + }, [t]); + return (
{technician ? : null} diff --git a/client/src/graphql/apollo-error-handling.js b/client/src/graphql/apollo-error-handling.js index 1ee2c1d8e..1d3f4e857 100644 --- a/client/src/graphql/apollo-error-handling.js +++ b/client/src/graphql/apollo-error-handling.js @@ -1,17 +1,21 @@ import { onError } from "@apollo/client/link/error"; //https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth +import * as Sentry from "@sentry/react"; const errorLink = onError( ({ graphQLErrors, networkError, operation, forward }) => { - if (graphQLErrors) - graphQLErrors.forEach(({ message, locations, path }) => + if (graphQLErrors) { + graphQLErrors.forEach(({ message, locations, path }) => { console.log( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}` - ) - ); + ); + Sentry.captureException({ message, locations, path }); + }); + } if (networkError) console.log(`[Network error]: ${JSON.stringify(networkError)}`); console.log(operation.getContext()); + return forward(operation); } ); diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js index a5bcbea55..eef9a759f 100644 --- a/client/src/graphql/courtesy-car.queries.js +++ b/client/src/graphql/courtesy-car.queries.js @@ -22,6 +22,7 @@ export const QUERY_AVAILABLE_CC = gql` ] status: { _eq: "courtesycars.status.in" } } + order_by: { fleetnumber: asc } ) { color dailycost @@ -29,16 +30,17 @@ export const QUERY_AVAILABLE_CC = gql` fleetnumber fuel id + insuranceexpires make - model - plate - status - year - dailycost mileage + model notes nextservicekm nextservicedate + plate + readiness + status + year } } `; @@ -57,7 +59,7 @@ export const CHECK_CC_FLEET_NUMBER = gql` `; export const QUERY_ALL_CC = gql` query QUERY_ALL_CC { - courtesycars { + courtesycars(order_by: { fleetnumber: asc }) { color created_at dailycost @@ -68,19 +70,20 @@ export const QUERY_ALL_CC = gql` insuranceexpires leaseenddate make + mileage model nextservicedate nextservicekm notes plate purchasedate + readiness registrationexpires serviceenddate servicestartdate status vin year - mileage cccontracts( where: { status: { _eq: "contracts.status.out" } } order_by: { contract_date: desc } @@ -90,10 +93,10 @@ export const QUERY_ALL_CC = gql` scheduledreturn job { id - ro_number ownr_fn ownr_ln ownr_co_nm + ro_number } } } @@ -119,19 +122,20 @@ export const QUERY_CC_BY_PK = gql` insuranceexpires leaseenddate make + mileage model nextservicedate nextservicekm notes plate purchasedate + readiness registrationexpires serviceenddate servicestartdate status vin year - mileage cccontracts_aggregate { aggregate { count(distinct: true) @@ -139,21 +143,20 @@ export const QUERY_CC_BY_PK = gql` } cccontracts(offset: $offset, limit: $limit, order_by: $order) { agreementnumber + driver_fn + driver_ln id - status - start - scheduledreturn kmstart kmend - driver_ln - driver_fn + scheduledreturn + start + status job { - ro_number - + id ownr_ln ownr_fn ownr_co_nm - id + ro_number } } } diff --git a/client/src/graphql/csi.queries.js b/client/src/graphql/csi.queries.js index f835f1d56..d2b62af43 100644 --- a/client/src/graphql/csi.queries.js +++ b/client/src/graphql/csi.queries.js @@ -57,19 +57,15 @@ export const INSERT_CSI = gql` `; export const QUERY_CSI_RESPONSE_PAGINATED = gql` - query QUERY_CSI_RESPONSE_PAGINATED( - $offset: Int - $limit: Int - $order: [csi_order_by!]! - ) { - csi(offset: $offset, limit: $limit, order_by: $order) { + query QUERY_CSI_RESPONSE_PAGINATED { + csi(order_by: { completedon: desc_nulls_last }) { id completedon job { ownr_fn ownr_ln + ownerid ro_number - id } } @@ -83,6 +79,7 @@ export const QUERY_CSI_RESPONSE_PAGINATED = gql` export const QUERY_CSI_RESPONSE_BY_PK = gql` query QUERY_CSI_RESPONSE_BY_PK($id: uuid!) { csi_by_pk(id: $id) { + completedon relateddata valid validuntil diff --git a/client/src/graphql/employees.queries.js b/client/src/graphql/employees.queries.js index 207c10bab..e34d2be3b 100644 --- a/client/src/graphql/employees.queries.js +++ b/client/src/graphql/employees.queries.js @@ -3,11 +3,12 @@ import { gql } from "@apollo/client"; export const QUERY_EMPLOYEES = gql` query QUERY_EMPLOYEES { employees(order_by: { employee_number: asc }) { - last_name - id + active + employee_number first_name flat_rate - employee_number + id + last_name } } `; diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index b18673de6..650abc302 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -5,7 +5,7 @@ export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql` $offset: Int $limit: Int $order: [jobs_order_by!] - $statuses: [String!]!, + $statuses: [String!]! $isConverted: Boolean ) { jobs( @@ -108,22 +108,19 @@ export const QUERY_ALL_ACTIVE_JOBS = gql` `; export const QUERY_PARTS_QUEUE = gql` - query QUERY_PARTS_QUEUE( - $statuses: [String!]! - $offset: Int - $limit: Int - $order: [jobs_order_by!] - ) { + query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) { jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) { aggregate { count(distinct: true) } } jobs( - where: { _and: [{ status: { _in: $statuses }, converted: { _eq: true } }] } + where: { + _and: [{ status: { _in: $statuses }, converted: { _eq: true } }] + } offset: $offset limit: $limit - order_by: $order + order_by: { ro_number: desc } ) { ownr_fn ownr_ln @@ -140,7 +137,9 @@ export const QUERY_PARTS_QUEUE = gql` v_color vehicleid scheduled_in + scheduled_completion id + ins_co_nm clm_no ro_number status @@ -336,6 +335,7 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` category iouparent ro_number + ownerid ownr_fn ownr_ln ownr_co_nm @@ -364,6 +364,8 @@ export const QUERY_JOBS_IN_PRODUCTION = gql` employee_refinish employee_prep employee_csr + est_ct_fn + est_ct_ln suspended date_repairstarted joblines_status { @@ -540,147 +542,166 @@ export const QUERY_JOB_COSTING_DETAILS = gql` export const GET_JOB_BY_PK = gql` query GET_JOB_BY_PK($id: uuid!) { jobs_by_pk(id: $id) { - updated_at + actual_completion + actual_delivery + actual_in + adjustment_bottom_line + area_of_damage + auto_add_ats + available_jobs { + id + } + alt_transport + ca_bc_pvrt + ca_customer_gst + ca_gst_registrant + category + cccontracts { + agreementnumber + courtesycar { + fleetnumber + id + make + model + plate + year + } + id + scheduledreturn + start + status + } + cieca_ttl + class + clm_no + clm_total + comment + converted + csiinvites { + completedon + id + } + date_estimated + date_exported + date_invoiced + date_last_contacted + date_lost_sale + date_next_contact + date_open + date_rentalresp + date_repairstarted + date_scheduled + date_towin + date_void + ded_amt + ded_note + ded_status + deliverchecklist + depreciation_taxes + driveable + employee_body employee_body_rel { id first_name last_name } - employee_refinish_rel { - id - first_name - last_name - } - employee_prep_rel { - id - first_name - last_name - } + employee_csr employee_csr_rel { id first_name last_name } - employee_csr employee_prep + employee_prep_rel { + id + first_name + last_name + } employee_refinish - employee_body - alt_transport - intakechecklist - invoice_final_note - comment - loss_desc - kmin - kmout - referral_source - referral_source_extra - unit_number - po_number - special_coverage_policy - scheduled_delivery - converted - lbr_adjustments - ro_number - po_number - clm_total + employee_refinish_rel { + id + first_name + last_name + } + est_co_nm + est_ct_fn + est_ct_ln + est_ea + est_ph1 + federal_tax_rate + id inproduction - vehicleid - plate_no - plate_st - v_vin - v_model_yr - v_model_desc - v_make_desc - v_color - vehicleid - driveable - towin - loss_of_use - lost_sale_reason - vehicle { - id - plate_no - plate_st - v_vin - v_model_yr - v_model_desc - v_make_desc - v_color - notes - v_paint_codes - jobs { - id - ro_number - status - clm_no - } - } - available_jobs { - id - } - ins_co_id - policy_no - loss_date - clm_no - area_of_damage - ins_co_nm ins_addr1 ins_city + ins_co_id + ins_co_nm ins_ct_ln ins_ct_fn ins_ea ins_ph1 - est_co_nm - est_ct_fn - est_ct_ln - est_ph1 - est_ea - selling_dealer - servicing_dealer - selling_dealer_contact - servicing_dealer_contact - regie_number - scheduled_completion - id - ded_amt - ded_status - depreciation_taxes - other_amount_payable - towing_payable - storage_payable - adjustment_bottom_line - federal_tax_rate - state_tax_rate - local_tax_rate - tax_tow_rt - tax_str_rt - tax_paint_mat_rt - tax_shop_mat_rt - tax_sub_rt - tax_lbr_rt - tax_levies_rt - parts_tax_rates - job_totals - ownr_fn - ownr_ln - ownr_co_nm - ownr_ea - ownr_addr1 - ownr_addr2 - ownr_city - ownr_st - ownr_zip - ownr_ctry - ownr_ph1 - ownr_ph2 - production_vars - ca_gst_registrant - ownerid - ded_note - materials - auto_add_ats - rate_ats + intakechecklist + invoice_final_note iouparent + job_totals + joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { + act_price + ah_detail_line + alt_partm + alt_partno + billlines(limit: 1, order_by: { bill: { date: desc } }) { + actual_cost + actual_price + bill { + id + invoice_number + vendor { + id + name + } + } + joblineid + id + quantity + } + convertedtolbr + critical + db_hrs + db_price + db_ref + id + ioucreated + lbr_amt + lbr_op + line_desc + line_ind + line_no + line_ref + location + manual_line + mod_lb_hrs + mod_lbr_ty + notes + oem_partno + op_code_desc + part_qty + part_type + prt_dsmk_m + prt_dsmk_p + status + tax_part + unq_seq + } + kmin + kmout + labor_rate_desc + lbr_adjustments + local_tax_rate + loss_date + loss_desc + loss_of_use + lost_sale_reason + materials + other_amount_payable owner { id ownr_fn @@ -697,7 +718,40 @@ export const GET_JOB_BY_PK = gql` ownr_ph2 tax_number } - labor_rate_desc + owner_owing + ownerid + ownr_addr1 + ownr_addr2 + ownr_ctry + ownr_city + ownr_co_nm + ownr_ea + ownr_fn + ownr_ln + ownr_ph1 + ownr_ph2 + ownr_st + ownr_zip + parts_tax_rates + payments { + amount + created_at + date + exportedat + id + jobid + memo + payer + paymentnum + transactionid + type + } + plate_no + plate_st + po_number + policy_no + production_vars + rate_ats rate_la1 rate_la2 rate_la3 @@ -721,121 +775,64 @@ export const GET_JOB_BY_PK = gql` rate_mapa rate_mash rate_matd - actual_in - federal_tax_rate - local_tax_rate - state_tax_rate + regie_number + referral_source + referral_source_extra + remove_from_ar + ro_number scheduled_completion - scheduled_in - actual_completion scheduled_delivery - actual_delivery - date_estimated - date_open - date_scheduled - date_invoiced - date_last_contacted - date_lost_sale - date_next_contact - date_towin - date_rentalresp - date_exported - date_repairstarted - date_void + scheduled_in + selling_dealer + servicing_dealer + selling_dealer_contact + servicing_dealer_contact + special_coverage_policy + state_tax_rate status - owner_owing - tax_registration_number - class - category - deliverchecklist - voided - ca_bc_pvrt - ca_customer_gst + storage_payable suspended - joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { + tax_lbr_rt + tax_levies_rt + tax_paint_mat_rt + tax_registration_number + tax_shop_mat_rt + tax_str_rt + tax_sub_rt + tax_tow_rt + towin + towing_payable + unit_number + updated_at + v_vin + v_model_yr + v_model_desc + v_make_desc + v_color + vehicleid + vehicle { id - alt_partm - line_no - unq_seq - line_ind - line_desc - line_ref - part_type - oem_partno - alt_partno - db_price - act_price - part_qty - mod_lbr_ty - db_hrs - mod_lb_hrs - lbr_op - lbr_amt - op_code_desc - status + jobs { + clm_no + id + ro_number + status + } notes - location - tax_part - db_ref - manual_line - prt_dsmk_p - prt_dsmk_m - ioucreated - convertedtolbr - ah_detail_line - critical - billlines(limit: 1, order_by: { bill: { date: desc } }) { - id - quantity - actual_cost - actual_price - joblineid - bill { - id - invoice_number - vendor { - id - name - } - } - } - } - payments { - id - jobid - amount - payer - paymentnum - created_at - transactionid - memo - date - type - exportedat - } - cccontracts { - id - status - start - scheduledreturn - agreementnumber - courtesycar { - id - make - model - year - plate - fleetnumber - } - } - cieca_ttl - csiinvites { - id - completedon + plate_no + plate_st + v_color + v_make_desc + v_model_desc + v_model_yr + v_paint_codes + v_vin } + voided } } `; + export const GET_JOB_RECONCILIATION_BY_PK = gql` query GET_JOB_RECONCILIATION_BY_PK($id: uuid!) { bills(where: { jobid: { _eq: $id } }) { @@ -900,6 +897,7 @@ export const GET_JOB_RECONCILIATION_BY_PK = gql` } } `; + export const QUERY_JOB_CARD_DETAILS = gql` query QUERY_JOB_CARD_DETAILS($id: uuid!) { jobs_by_pk(id: $id) { @@ -2220,3 +2218,280 @@ export const GET_JOB_LINE_ORDERS = gql` } } `; + +export const UPDATE_REMOVE_FROM_AR = gql` + mutation UPDATE_REMOVE_FROM_AR($jobId: uuid!, $remove_from_ar: Boolean!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { remove_from_ar: $remove_from_ar } + ) { + id + remove_from_ar + } + } +`; + +export const UNVOID_JOB = gql` + mutation UNVOID_JOB( + $jobId: uuid! + $default_imported: String! + $currentUserEmail: String! + $text: String! + ) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { voided: false, status: $default_imported, date_void: null } + ) { + id + date_void + voided + status + } + insert_notes( + objects: { + jobid: $jobId + audit: true + created_by: $currentUserEmail + text: $text + } + ) { + returning { + id + } + } + } +`; + +export const DELETE_INTAKE_CHECKLIST = gql` + mutation DELETE_INTAKE($jobId: uuid!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { intakechecklist: null } + ) { + id + intakechecklist + } + } +`; + +export const DELETE_DELIVERY_CHECKLIST = gql` + mutation DELETE_DELIVERY($jobId: uuid!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { deliverchecklist: null } + ) { + id + deliverchecklist + } + } +`; + +export const MARK_JOB_FOR_REEXPORT = gql` + mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!, $default_invoiced: String!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { date_exported: null, status: $default_invoiced } + ) { + id + date_exported + status + date_invoiced + } + } +`; + +export const MARK_JOB_AS_EXPORTED = gql` + mutation MARK_JOB_AS_EXPORTED( + $jobId: uuid! + $date_exported: timestamptz! + $default_exported: String! + ) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { date_exported: $date_exported, status: $default_exported } + ) { + id + date_exported + date_invoiced + status + } + } +`; + +export const MARK_JOB_AS_UNINVOICED = gql` + mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, $default_delivered: String!) { + update_jobs_by_pk( + pk_columns: { id: $jobId } + _set: { + date_exported: null + date_invoiced: null + status: $default_delivered + } + ) { + id + date_exported + date_invoiced + status + } + } +`; + +export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql` + query QUERY_JOB_CARD_DETAILS($id: uuid!) { + jobs_by_pk(id: $id) { + actual_completion + actual_delivery + actual_in + alt_transport + available_jobs { + id + } + area_of_damage + ca_gst_registrant + cccontracts { + agreementnumber + courtesycar { + id + make + model + year + plate + fleetnumber + } + id + scheduledreturn + start + status + } + clm_no + clm_total + comment + date_estimated + date_exported + date_invoiced + date_last_contacted + date_next_contact + date_open + date_repairstarted + date_scheduled + ded_amt + employee_body + employee_body_rel { + id + first_name + last_name + } + employee_csr + employee_csr_rel { + id + first_name + last_name + } + employee_prep + employee_prep_rel { + id + first_name + last_name + } + employee_refinish + employee_refinish_rel { + id + first_name + last_name + } + est_co_nm + est_ct_fn + est_ct_ln + est_ea + est_ph1 + id + ins_co_nm + ins_ct_fn + ins_ct_ln + ins_ea + ins_ph1 + inproduction + job_totals + joblines( + order_by: { line_no: asc } + where: { + part_type: { + _in: [ + "PAN" + "PAC" + "PAR" + "PAL" + "PAA" + "PAM" + "PAP" + "PAG" + ] + } + removed: { _eq: false } + } + ) { + act_price + alt_partno + db_ref + id + line_desc + line_no + location + mod_lbr_ty + mod_lb_hrs + oem_partno + part_qty + part_type + prt_dsmk_m + status + } + lbr_adjustments + ownr_co_nm + ownr_ea + ownr_fn + ownr_ln + ownr_ph1 + ownr_ph2 + owner { + id + allow_text_message + preferred_contact + tax_number + } + owner_owing + plate_no + plate_st + po_number + production_vars + ro_number + scheduled_completion + scheduled_delivery + scheduled_in + special_coverage_policy + status + suspended + updated_at + vehicle { + id + jobs { + id + clm_no + ro_number + } + notes + plate_no + v_color + v_make_desc + v_model_desc + v_model_yr + } + vehicleid + v_color + v_make_desc + v_model_desc + v_model_yr + v_vin + voided + } + } +`; diff --git a/client/src/graphql/timetickets.queries.js b/client/src/graphql/timetickets.queries.js index bb90c4e9d..9f3dec512 100644 --- a/client/src/graphql/timetickets.queries.js +++ b/client/src/graphql/timetickets.queries.js @@ -143,9 +143,14 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` $end: date! $fixedStart: date! $fixedEnd: date! + $jobStart: timestamptz! + $jobEnd: timestamptz! ) { timetickets( - where: { date: { _gte: $start, _lte: $end }, cost_center: {_neq: "timetickets.labels.shift"} } + where: { + date: { _gte: $start, _lte: $end } + cost_center: { _neq: "timetickets.labels.shift" } + } order_by: { date: desc_nulls_first } ) { actualhrs @@ -176,7 +181,10 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` } } fixedperiod: timetickets( - where: { date: { _gte: $fixedStart, _lte: $fixedEnd }, cost_center: {_neq: "timetickets.labels.shift"} } + where: { + date: { _gte: $fixedStart, _lte: $fixedEnd } + cost_center: { _neq: "timetickets.labels.shift" } + } order_by: { date: desc_nulls_first } ) { actualhrs @@ -205,6 +213,25 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql` last_name } } + jobs( + where: { + date_invoiced: { _is_null: true } + ro_number: { _is_null: false } + voided: { _eq: false } + _or: [ + { actual_completion: { _gte: $jobStart, _lte: $jobEnd } } + { actual_delivery: { _gte: $jobStart, _lte: $jobEnd } } + ] + } + ) { + id + joblines(order_by: { line_no: asc }, where: { removed: { _eq: false } }) { + convertedtolbr + convertedtolbr_data + mod_lb_hrs + mod_lbr_ty + } + } } `; diff --git a/client/src/index.js b/client/src/index.js index 2006eccee..c94644d3b 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -14,38 +14,38 @@ import { persistor, store } from "./redux/store"; import reportWebVitals from "./reportWebVitals"; import "./translations/i18n"; import "./utils/CleanAxios"; + //import { BrowserTracing } from "@sentry/tracing"; // Dinero.defaultCurrency = "CAD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; -if (process.env.NODE_ENV !== "development") { - Sentry.init({ - dsn: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027", - ignoreErrors: [ - "ResizeObserver loop", - "Module specifier, 'fs' does not start", - "Module specifier, 'zlib' does not start with", - ], - integrations: [ - // new BrowserTracing(), - // new Sentry.Integrations.Breadcrumbs({ console: true }), - // new Sentry.Replay(), - ], - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - // replaysSessionSampleRate: 0.1, - // // If the entire session is not sampled, use the below sample rate to sample - // // sessions when an error occurs. - // replaysOnErrorSampleRate: 1.0, - environment: process.env.NODE_ENV, - // tracesSampleRate: 0.2, - // We recommend adjusting this value in production, or using tracesSampler - // for finer control - // tracesSampleRate: 0.5, - }); -} +//if (process.env.NODE_ENV !== "development") { +Sentry.init({ + dsn: "https://fd7e89369b6b4bdc9c6c4c9f22fa4ee4@o492140.ingest.sentry.io/5651027", + ignoreErrors: [ + "ResizeObserver loop", + "Module specifier, 'fs' does not start", + "Module specifier, 'zlib' does not start with", + ], + integrations: [ + Sentry.replayIntegration({ + maskAllText: false, + blockAllMedia: true, + }), + new Sentry.BrowserTracing({}), + ], + tracePropagationTargets: [ + "api.imex.online", + "api.test.imex.online", + "db.imex.online", + ], + tracesSampleRate: 1.0, + replaysOnErrorSampleRate: 1.0, + environment: process.env.NODE_ENV, +}); +//} ReactDOM.render( diff --git a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx index 93bffe97f..bf73b4cc3 100644 --- a/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx +++ b/client/src/pages/courtesy-car-detail/courtesy-car-detail.page.container.jsx @@ -1,11 +1,14 @@ import { useMutation, useQuery } from "@apollo/client"; import { Form, notification } from "antd"; import moment from "moment"; +import queryString from "query-string"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { useParams } from "react-router-dom"; +import { useLocation, useParams } from "react-router-dom"; import AlertComponent from "../../components/alert/alert.component"; +import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; +import NotFound from "../../components/not-found/not-found.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { QUERY_CC_BY_PK, UPDATE_CC } from "../../graphql/courtesy-car.queries"; import { @@ -13,13 +16,10 @@ import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; +import { pageLimit } from "../../utils/config"; import { CreateRecentItem } from "../../utils/create-recent-item"; +import UndefinedToNull from "./../../utils/undefinedtonull"; import CourtesyCarDetailPageComponent from "./courtesy-car-detail.page.component"; -import NotFound from "../../components/not-found/not-found.component"; -import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; -import queryString from "query-string"; -import { useLocation } from "react-router-dom"; -import {pageLimit} from "../../utils/config"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -112,7 +112,10 @@ export function CourtesyCarDetailPageContainer({ setSaveLoading(true); const result = await updateCourtesyCar({ - variables: { cc: { ...values }, ccId: ccId }, + variables: { + cc: { ...UndefinedToNull(values, ["readiness"]) }, + ccId: ccId, + }, refetchQueries: ["QUERY_CC_BY_PK"], awaitRefetchQueries: true, }); diff --git a/client/src/pages/csi/csi.container.page.jsx b/client/src/pages/csi/csi.container.page.jsx index 4076a01a4..74113ae20 100644 --- a/client/src/pages/csi/csi.container.page.jsx +++ b/client/src/pages/csi/csi.container.page.jsx @@ -1,88 +1,67 @@ -import { useQuery, useMutation } from "@apollo/client"; -import { Form, Layout, Typography, Button, Result } from "antd"; -import React, { useState } from "react"; +// import { useMutation, useQuery } from "@apollo/client"; +import { Button, Form, Layout, Result, Typography } from "antd"; +import axios from "axios"; +import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; import { useParams } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; import AlertComponent from "../../components/alert/alert.component"; import ConfigFormComponents from "../../components/config-form-components/config-form-components.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; -import { QUERY_SURVEY, COMPLETE_SURVEY } from "../../graphql/csi.queries"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; import { selectCurrentUser } from "../../redux/user/user.selectors"; +import { DateTimeFormat } from "./../../utils/DateFormatter"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, }); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); +const mapDispatchToProps = (dispatch) => ({}); + export default connect(mapStateToProps, mapDispatchToProps)(CsiContainerPage); export function CsiContainerPage({ currentUser }) { const { surveyId } = useParams(); const [form] = Form.useForm(); + const [axiosResponse, setAxiosResponse] = useState(null); const [submitting, setSubmitting] = useState({ loading: false, submitted: false, }); - - const { loading, error, data } = useQuery(QUERY_SURVEY, { - variables: { surveyId }, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); - const { t } = useTranslation(); - const [completeSurvey] = useMutation(COMPLETE_SURVEY); - if (loading) return ; - if (error || !!!data.csi_by_pk) - return ( -
- - {error ? ( -
ERROR: {error.graphQLErrors.map((e) => e.message)}
- ) : null} -
-
- ); - - const handleFinish = async (values) => { - setSubmitting({ ...submitting, loading: true }); - - const result = await completeSurvey({ - variables: { - surveyId, - survey: { - response: values, - valid: false, - completedon: new Date(), - }, - }, - }); - - if (!!!result.errors) { - setSubmitting({ ...submitting, loading: false, submitted: true }); - } else { - setSubmitting({ - ...submitting, + const getAxiosData = useCallback(async () => { + try { + try { + window.$crisp.push(["do", "chat:hide"]); + } catch { + console.log("Unable to attach to crisp instance. "); + } + setSubmitting((prevSubmitting) => ({ ...prevSubmitting, loading: true })); + const response = await axios.post("/csi/lookup", { surveyId }); + setSubmitting((prevSubmitting) => ({ + ...prevSubmitting, loading: false, - error: JSON.stringify(result.errors), + })); + setAxiosResponse(response.data); + } catch (error) { + console.error(`Something went wrong...: ${error.message}`); + console.dir({ + stack: error?.stack, + message: error?.message, }); } - }; + }, [setAxiosResponse, surveyId]); - const { - relateddata: { bodyshop, job }, - csiquestion: { config: csiquestions }, - } = data.csi_by_pk; + useEffect(() => { + getAxiosData().catch((err) => + console.error( + `Something went wrong fetching axios data: ${err.message || ""}` + ) + ); + }, [getAxiosData]); - if (currentUser && currentUser.authorized) + // Return if authorized + if (currentUser && currentUser.authorized) { return ( ); + } - return ( - -
-
- {bodyshop.logo_img_path && bodyshop.logo_img_path.src ? ( - Logo - ) : null} -
- {bodyshop.shopname || ""} -
{`${bodyshop.address1 || ""}`}
-
{`${bodyshop.address2 || ""}`}
-
{`${bodyshop.city || ""} ${bodyshop.state || ""} ${ - bodyshop.zip_post || "" - }`}
+ if (submitting.loading) return ; + + const handleFinish = async (values) => { + try { + setSubmitting({ ...submitting, loading: true, submitting: true }); + const result = await axios.post("/csi/submit", { surveyId, values }); + console.log("result", result); + if (!!!result.errors && result.data.update_csi.affected_rows > 0) { + setSubmitting({ ...submitting, loading: false, submitted: true }); + } + } catch (error) { + console.error(`Something went wrong...: ${error.message}`); + console.dir({ + stack: error?.stack, + message: error?.message, + }); + } + }; + + if (!axiosResponse || axiosResponse.csi_by_pk === null) { + // Do something here , this is where you would return a loading box or something + return ( + <> + + + + + + + + {t("csi.labels.copyright")}{" "} + {t("csi.fields.surveyid", { surveyId: surveyId })} + + + + ); + } else { + const { + relateddata: { bodyshop, job }, + csiquestion: { config: csiquestions }, + } = axiosResponse.csi_by_pk; + + return ( + +
+
+ {bodyshop.logo_img_path && bodyshop.logo_img_path.src ? ( + {bodyshop.shopname.concat(" + ) : null} +
+ + {bodyshop.shopname || ""} + + + {`${bodyshop.address1 || ""}${bodyshop.address2 ? ", " : ""}${ + bodyshop.address2 || "" + }`.trim()} + + + {`${bodyshop.city || ""}${ + bodyshop.city && bodyshop.state ? ", " : "" + }${bodyshop.state || ""} ${bodyshop.zip_post || ""}`.trim()} + +
+ {t("csi.labels.title")} + + {t("csi.labels.greeting", { + name: job.ownr_co_nm || job.ownr_fn || "", + })} + + + {t("csi.labels.intro", { shopname: bodyshop.shopname || "" })} +
- {t("csi.labels.title")} - {`Hi ${job.ownr_co_nm || job.ownr_fn || ""}!`} - - {`At ${ - bodyshop.shopname || "" - }, we value your feedback. We would love to - hear what you have to say. Please fill out the form below.`} - -
- {submitting.error ? ( - - ) : null} + {submitting.error ? ( + + ) : null} - {submitting.submitted ? ( - - - - ) : ( - -
- - - -
- )} - - - {`Copyright ImEX.Online. Survey ID: ${surveyId}`} - - - ); + {submitting.submitted ? ( + + + + ) : ( + +
+ {axiosResponse.csi_by_pk.valid ? ( + <> + + + + ) : ( + <> + + + {t("csi.successes.submittedsub")} + + + )} + +
+ )} + + {t("csi.labels.copyright")}{" "} + {t("csi.fields.surveyid", { surveyId: surveyId })} + + + ); + } } diff --git a/client/src/pages/jobs-admin/jobs-admin.page.jsx b/client/src/pages/jobs-admin/jobs-admin.page.jsx index 0d08e5114..0c7eb9a33 100644 --- a/client/src/pages/jobs-admin/jobs-admin.page.jsx +++ b/client/src/pages/jobs-admin/jobs-admin.page.jsx @@ -7,16 +7,16 @@ import { useParams } from "react-router-dom"; import AlertComponent from "../../components/alert/alert.component"; import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component"; import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component"; +import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component"; import JobsAdminClass from "../../components/jobs-admin-class/jobs-admin-class.component"; import JobsAdminDatesChange from "../../components/jobs-admin-dates/jobs-admin-dates.component"; import JobsAdminDeleteIntake from "../../components/jobs-admin-delete-intake/jobs-admin-delete-intake.component"; import JobsAdminMarkReexport from "../../components/jobs-admin-mark-reexport/jobs-admin-mark-reexport.component"; import JobAdminOwnerReassociate from "../../components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component"; +import JobsAdminRemoveAR from "../../components/jobs-admin-remove-ar/jobs-admin-remove-ar.component"; import JobsAdminUnvoid from "../../components/jobs-admin-unvoid/jobs-admin-unvoid.component"; import JobAdminVehicleReassociate from "../../components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; -import JobsAdminStatus from "../../components/jobs-admin-change-status/jobs-admin-change.status.component"; - import NotFound from "../../components/not-found/not-found.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { GET_JOB_BY_PK } from "../../graphql/jobs.queries"; @@ -104,6 +104,7 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader }) { + diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 15af524ed..dfe83358c 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -1,6 +1,6 @@ -import _ from "lodash"; import { useLazyQuery, useMutation } from "@apollo/client"; import { Form, notification } from "antd"; +import _ from "lodash"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -90,6 +90,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { {}, values, { date_open: new Date() }, + { date_estimated: new Date() }, { vehicle: state.vehicle.selectedid || state.vehicle.none diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 001a18166..89a912242 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -54,6 +54,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import UndefinedToNull from "../../utils/undefinedtonull"; import { DateTimeFormat } from "./../../utils/DateFormatter"; +import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -333,7 +334,15 @@ export function JobsDetailPage({ > - {t('menus.jobsdetail.lifecycle')}} + key="lifecycle" + > + + + + diff --git a/client/src/pages/parts-queue/parts-queue.page.container.jsx b/client/src/pages/parts-queue/parts-queue.page.container.jsx index 5f5d8d617..3f78e9b92 100644 --- a/client/src/pages/parts-queue/parts-queue.page.container.jsx +++ b/client/src/pages/parts-queue/parts-queue.page.container.jsx @@ -1,12 +1,13 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; +import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component"; +import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; -import PartsQueuePage from "./parts-queue.page.component"; const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), @@ -26,7 +27,8 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) { return ( - + + ); } diff --git a/client/src/pages/shop-csi/shop-csi.container.page.jsx b/client/src/pages/shop-csi/shop-csi.container.page.jsx index f0c295685..e9d304b53 100644 --- a/client/src/pages/shop-csi/shop-csi.container.page.jsx +++ b/client/src/pages/shop-csi/shop-csi.container.page.jsx @@ -1,22 +1,20 @@ -import { Row, Col } from "antd"; import { useQuery } from "@apollo/client"; -import queryString from "query-string"; +import { Col, Row } from "antd"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import AlertComponent from "../../components/alert/alert.component"; import CsiResponseFormContainer from "../../components/csi-response-form/csi-response-form.container"; import CsiResponseListPaginated from "../../components/csi-response-list-paginated/csi-response-list-paginated.component"; +import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import { QUERY_CSI_RESPONSE_PAGINATED } from "../../graphql/csi.queries"; import { setBreadcrumbs, setSelectedHeader, } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; -import {pageLimit} from "../../utils/config"; + const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -33,28 +31,11 @@ export function ShopCsiContainer({ }) { const { t } = useTranslation(); - const searchParams = queryString.parse(useLocation().search); - const { page, sortcolumn, sortorder } = searchParams; - const { loading, error, data, refetch } = useQuery( QUERY_CSI_RESPONSE_PAGINATED, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", - variables: { - //search: search || "", - offset: page ? (page - 1) * pageLimit : 0, - limit: pageLimit, - order: [ - { - [sortcolumn || "completedon"]: sortorder - ? sortorder === "descend" - ? "desc_nulls_last" - : "asc" - : "desc_nulls_last", - }, - ], - }, } ); @@ -73,12 +54,7 @@ export function ShopCsiContainer({ if (error) return ; return ( - - // } - > +
{ + document.title = t("titles.techjobclock"); + }, [t]); + return (
diff --git a/client/src/pages/tech-lookup/tech-lookup.container.jsx b/client/src/pages/tech-lookup/tech-lookup.container.jsx index 1297220b9..e24f8f661 100644 --- a/client/src/pages/tech-lookup/tech-lookup.container.jsx +++ b/client/src/pages/tech-lookup/tech-lookup.container.jsx @@ -1,9 +1,16 @@ -import React from "react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component"; import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component"; import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component"; export default function TechLookupContainer() { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.techjoblookup"); + }, [t]); + return (
diff --git a/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx b/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx index 52e0e99c7..2dad65e64 100644 --- a/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx +++ b/client/src/pages/tech-shift-clock/tech-shift-clock.component.jsx @@ -1,7 +1,14 @@ -import React from "react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import TimeTicketShift from "../../components/time-ticket-shift/time-ticket-shift.container"; export default function TechShiftClock() { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.techshiftclock"); + }, [t]); + return (
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index cf55cfeb2..501c016ed 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "ADMIN: Remove from AR updated to: {{status}}", "admin_jobmarkexported": "ADMIN: Job marked as exported.", "admin_jobmarkforreexport": "ADMIN: Job marked for re-export.", "admin_jobuninvoice": "ADMIN: Job has been uninvoiced.", @@ -112,11 +113,11 @@ "jobassignmentchange": "Employee {{name}} assigned to {{operation}}", "jobassignmentremoved": "Employee assignment removed for {{operation}}", "jobchecklist": "Checklist type \"{{type}}\" completed. In production set to {{inproduction}}. Status set to {{status}}.", - "jobinvoiced": "Job has been invoiced.", "jobconverted": "Job converted and assigned number {{ro_number}}.", "jobfieldchanged": "Job field $t(jobs.fields.{{field}}) changed to {{value}}.", "jobimported": "Job imported.", "jobinproductionchange": "Job production status set to {{inproduction}}", + "jobinvoiced": "Job has been invoiced.", "jobioucreated": "IOU Created.", "jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.", "jobnoteadded": "Note added to Job.", @@ -205,6 +206,7 @@ "entered_total": "Total of Entered Lines", "enteringcreditmemo": "You are entering a credit memo. Please ensure you are also entering positive values.", "federal_tax": "Federal Tax", + "federal_tax_exempt": "Federal Tax Exempt?", "generatepartslabel": "Generate Parts Labels after Saving?", "iouexists": "An IOU exists that is associated to this RO.", "local_tax": "Local Tax", @@ -213,6 +215,7 @@ "new": "New Bill", "noneselected": "No bill selected.", "onlycmforinvoiced": "Only credit memos can be entered for any Job that has been invoiced, exported, or voided.", + "printlabels": "Print Labels", "retailtotal": "Bills Retail Total", "savewithdiscrepancy": "You are about to save this bill with a discrepancy. The system will continue to use the calculated amount using the bill lines. Press cancel to return to the bill.", "state_tax": "Provincial/State Tax", @@ -252,7 +255,6 @@ "saving": "Error encountered while saving. {{message}}" }, "fields": { - "ReceivableCustomField": "QBO Receivable Custom Field {{number}}", "address1": "Address 1", "address2": "Address 2", "appt_alt_transport": "Appointment Alternative Transportation Options", @@ -329,6 +331,9 @@ "md_ded_notes": "Deductible Notes", "md_email_cc": "Auto Email CC: $t(printcenter.subjects.jobs.{{template}})", "md_from_emails": "Additional From Emails", + "md_functionality_toggles": { + "parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue" + }, "md_hour_split": { "paint": "Paint Hour Split", "prep": "Prep Hour Split" @@ -351,9 +356,6 @@ }, "md_payment_types": "Payment Types", "md_referral_sources": "Referral Sources", - "md_functionality_toggles": { - "parts_queue_toggle": "Auto Add Imported/Supplemented Jobs to Parts Queue" - }, "md_tasks_presets": { "hourstype": "", "memo": "", @@ -449,6 +451,7 @@ "config": "Shop -> Config", "dashboard": "Shop -> Dashboard", "rbac": "Shop -> RBAC", + "reportcenter": "Shop -> Report Center", "templates": "Shop -> Templates", "vendors": "Shop -> Vendors" }, @@ -470,6 +473,7 @@ "editaccess": "Users -> Edit access" } }, + "ReceivableCustomField": "QBO Receivable Custom Field {{number}}", "responsibilitycenter": "Responsibility Center", "responsibilitycenter_accountdesc": "Account Description", "responsibilitycenter_accountitem": "Item", @@ -743,6 +747,7 @@ "driverinformation": "Driver's Information", "findcontract": "Find Contract", "findermodal": "Contract Finder", + "insuranceexpired": "The courtesy car insurance expires before the car is expected to return.", "noteconvertedfrom": "R.O. created from converted Courtesy Car Contract {{agreementnumber}}.", "populatefromjob": "Populate from Job", "rates": "Contract Rates", @@ -783,6 +788,7 @@ "notes": "Notes", "plate": "Plate Number", "purchasedate": "Purchase Date", + "readiness": "Readiness", "registrationexpires": "Registration Expires On", "serviceenddate": "Usage End Date", "servicestartdate": "Usage Start Date", @@ -810,6 +816,10 @@ "usage": "Usage", "vehicle": "Vehicle Description" }, + "readiness": { + "notready": "Not Ready", + "ready": "Ready" + }, "status": { "in": "Available", "inservice": "In Service", @@ -829,20 +839,27 @@ "creating": "Error creating survey {{message}}", "notconfigured": "You do not have any current CSI Question Sets configured.", "notfoundsubtitle": "We were unable to find a survey using the link you provided. Please ensure the URL is correct or reach out to your shop for more help.", - "notfoundtitle": "No survey found." + "notfoundtitle": "No survey found.", + "surveycompletetitle": "Survey previously completed", + "surveycompletesubtitle": "This survey was already completed on {{date}}." }, "fields": { "completedon": "Completed On", - "created_at": "Created At" + "created_at": "Created At", + "surveyid": "Survey ID {{surveyId}}", + "validuntil": "Valid Until" }, "labels": { - "nologgedinuser": "Please log out of ImEX Online", - "nologgedinuser_sub": "Users of ImEX Online cannot complete CSI surveys while logged in. Please log out and try again.", + "nologgedinuser": "Please log out of $t(titles.app)", + "nologgedinuser_sub": "Users of $t(titles.app) cannot complete CSI surveys while logged in. Please log out and try again.", "noneselected": "No response selected.", - "title": "Customer Satisfaction Survey" + "title": "Customer Satisfaction Survey", + "greeting": "Hi {{name}}!", + "intro": "At {{shopname}}, we value your feedback. We would love to hear what you have to say. Please fill out the form below.", + "copyright": "Copyright © $t(titles.app). All Rights Reserved." }, "successes": { - "created": "CSI created successfully. ", + "created": "CSI created successfully.", "submitted": "Your responses have been submitted successfully.", "submittedsub": "Your input is highly appreciated." } @@ -858,6 +875,7 @@ "labels": { "bodyhrs": "Body Hrs", "dollarsinproduction": "Dollars in Production", + "phone": "Phone", "prodhrs": "Production Hrs", "refhrs": "Refinish Hrs" }, @@ -873,8 +891,10 @@ "productiondollars": "Total Dollars in Production", "productionhours": "Total Hours in Production", "projectedmonthlysales": "Projected Monthly Sales", - "scheduledintoday": "Sheduled In Today: {{date}}", - "scheduledouttoday": "Sheduled Out Today: {{date}}" + "scheduledindate": "Sheduled In Today: {{date}}", + "scheduledintoday": "Sheduled In Today", + "scheduledoutdate": "Sheduled Out Today: {{date}}", + "scheduledouttoday": "Sheduled Out Today" } }, "dms": { @@ -993,10 +1013,13 @@ }, "labels": { "actions": "Actions", + "active": "Active", "endmustbeafterstart": "End date must be after start date.", "flat_rate": "Flat Rate", + "inactive": "Inactive", "name": "Name", "rate_type": "Rate Type", + "status": "Status", "straight_time": "Straight Time" }, "successes": { @@ -1202,6 +1225,31 @@ "updated": "Inventory line updated." } }, + "job_lifecycle": { + "columns": { + "duration": "Duration", + "end": "End", + "relative_end": "Relative End", + "relative_start": "Relative Start", + "start": "Start", + "value": "Value" + }, + "content": { + "current_status_accumulated_time": "Current Status Accumulated Time", + "data_unavailable": " There is currently no Lifecycle data for this Job.", + "legend_title": "Legend", + "loading": "Loading Job Timelines....", + "not_available": "N/A", + "previous_status_accumulated_time": "Previous Status Accumulated Time", + "title": "Job Lifecycle Component", + "title_durations": "Historical Status Durations", + "title_loading": "Loading", + "title_transitions": "Transitions" + }, + "errors": { + "fetch": "Error getting Job Lifecycle Data" + } + }, "job_payments": { "buttons": { "goback": "Go Back", @@ -1688,6 +1736,7 @@ "ca_gst_all_if_null": "If the Job is marked as a \"GST Registrant\" and this value is set to $0, the customer will be responsible for paying all of the GST by default. ", "calc_repair_days": "Calculated Repair Days", "calc_repair_days_tt": "This is the approximate number of days required to complete the repair according to the target touch time in your shop configuration (current set to {{target_touchtime}}).", + "calc_scheuled_completion": "Calculate Scheduled Completion", "cards": { "customer": "Customer Information", "damage": "Area of Damage", @@ -1696,6 +1745,7 @@ "estimator": "Estimator", "filehandler": "File Handler", "insurance": "Insurance Details", + "more": "More", "notes": "Notes", "parts": "Parts", "totals": "Totals", @@ -1784,6 +1834,7 @@ "override_header": "Override estimate header on import?", "ownerassociation": "Owner Association", "parts": "Parts", + "parts_lines": "Parts Lines", "parts_received": "Parts Rec.", "parts_tax_rates": "Parts Tax rates", "partsfilter": "Parts Only", @@ -1821,6 +1872,7 @@ }, "reconciliationheader": "Parts & Sublet Reconciliation", "relatedros": "Related ROs", + "remove_from_ar": "Remove from AR", "returntotals": "Return Totals", "rosaletotal": "RO Parts Total", "sale_additional": "Sales - Additional", @@ -1844,6 +1896,7 @@ "total_sales": "Total Sales", "totals": "Totals", "unvoidnote": "This Job was unvoided.", + "update_scheduled_completion": "Update Scheduled Completion?", "vehicle_info": "Vehicle", "vehicleassociation": "Vehicle Association", "viewallocations": "View Allocations", @@ -2000,6 +2053,7 @@ "general": "General", "insurance": "Insurance Information", "labor": "Labor", + "lifecycle": "Lifecycle", "partssublet": "Parts & Bills", "rates": "Rates", "repairdata": "Repair Data", @@ -2016,7 +2070,7 @@ "joblookup": "Job Lookup", "login": "Login", "logout": "Logout", - "productionboard": "Production Board - Visual", + "productionboard": "Production Visual", "productionlist": "Production List", "shiftclockin": "Shift Clock" } @@ -2046,6 +2100,9 @@ "sentby": "Sent by {{by}} at {{time}}", "typeamessage": "Send a message...", "unarchive": "Unarchive" + }, + "render": { + "conversation_list": "Conversation List" } }, "notes": { @@ -2521,6 +2578,16 @@ "generate": "Generate" }, "labels": { + "advanced_filters": "Advanced Filters and Sorters", + "advanced_filters_show": "Show", + "advanced_filters_hide": "Hide", + "advanced_filters_filters": "Filters", + "advanced_filters_sorters": "Sorters", + "advanced_filters_filter_field": "Field", + "advanced_filters_sorter_field": "Field", + "advanced_filters_sorter_direction": "Direction", + "advanced_filters_filter_operator": "Operator", + "advanced_filters_filter_value": "Value", "dates": "Dates", "employee": "Employee", "filterson": "Filters on {{object}}: {{field}}", @@ -2548,6 +2615,7 @@ }, "templates": { "anticipated_revenue": "Anticipated Revenue", + "ar_aging": "AR Aging", "attendance_detail": "Attendance (All Employees)", "attendance_employee": "Employee Attendance", "attendance_summary": "Attendance Summary (All Employees)", @@ -2610,6 +2678,7 @@ "open_orders": "Open Orders by Date", "open_orders_csr": "Open Orders by CSR", "open_orders_estimator": "Open Orders by Estimator", + "open_orders_excel": "Open Orders - Excel", "open_orders_ins_co": "Open Orders by Insurance Company", "open_orders_referral": "Open Orders by Referral Source", "open_orders_specific_csr": "Open Orders filtered by CSR", @@ -2697,6 +2766,7 @@ "efficiencyoverperiod": "Efficiency over Selected Dates", "entries": "Scoreboard Entries", "jobs": "Jobs", + "jobscompletednotinvoiced": "Completed Not Invoiced", "lastmonth": "Last Month", "lastweek": "Last Week", "monthlytarget": "Monthly", @@ -2711,6 +2781,7 @@ "timetickets": "Time Tickets", "timeticketsemployee": "Time Tickets by Employee", "todateactual": "Actual (MTD)", + "totalhrs": "Total Hours", "totaloverperiod": "Total over Selected Dates", "weeklyactual": "Actual (W)", "weeklytarget": "Weekly", @@ -2888,7 +2959,7 @@ "parts-queue": "Parts Queue | $t(titles.app)", "payments-all": "Payments | $t(titles.app)", "phonebook": "Phonebook | $t(titles.app)", - "productionboard": "Production - Board", + "productionboard": "Production Board - Visual | $t(titles.app)", "productionlist": "Production Board - List | $t(titles.app)", "profile": "My Profile | $t(titles.app)", "readyjobs": "Ready Jobs | $t(titles.app)", @@ -2900,6 +2971,10 @@ "shop-csi": "CSI Responses | $t(titles.app)", "shop-templates": "Shop Templates | $t(titles.app)", "shop_vendors": "Vendors | $t(titles.app)", + "techconsole": "Technician Console | $t(titles.app)", + "techjobclock": "Technician Job Clock | $t(titles.app)", + "techjoblookup": "Technician Job Lookup | $t(titles.app)", + "techshiftclock": "Technician Shift Clock | $t(titles.app)", "temporarydocs": "Temporary Documents | $t(titles.app)", "timetickets": "Time Tickets | $t(titles.app)", "ttapprovals": "", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index fd3bebe6f..c9b8d5b63 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "", "admin_jobmarkexported": "", "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", @@ -112,11 +113,11 @@ "jobassignmentchange": "", "jobassignmentremoved": "", "jobchecklist": "", - "jobinvoiced": "", "jobconverted": "", "jobfieldchanged": "", "jobimported": "", "jobinproductionchange": "", + "jobinvoiced": "", "jobioucreated": "", "jobmodifylbradj": "", "jobnoteadded": "", @@ -205,6 +206,7 @@ "entered_total": "", "enteringcreditmemo": "", "federal_tax": "", + "federal_tax_exempt": "", "generatepartslabel": "", "iouexists": "", "local_tax": "", @@ -213,6 +215,7 @@ "new": "", "noneselected": "", "onlycmforinvoiced": "", + "printlabels": "", "retailtotal": "", "savewithdiscrepancy": "", "state_tax": "", @@ -252,13 +255,9 @@ "saving": "" }, "fields": { - "ReceivableCustomField": "", "address1": "", "address2": "", "appt_alt_transport": "", - "md_functionality_toggles": { - "parts_queue_toggle": "" - }, "appt_colors": { "color": "", "label": "" @@ -332,6 +331,9 @@ "md_ded_notes": "", "md_email_cc": "", "md_from_emails": "", + "md_functionality_toggles": { + "parts_queue_toggle": "" + }, "md_hour_split": { "paint": "", "prep": "" @@ -449,6 +451,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, @@ -470,6 +473,7 @@ "editaccess": "" } }, + "ReceivableCustomField": "", "responsibilitycenter": "", "responsibilitycenter_accountdesc": "", "responsibilitycenter_accountitem": "", @@ -743,6 +747,7 @@ "driverinformation": "", "findcontract": "", "findermodal": "", + "insuranceexpired": "", "noteconvertedfrom": "", "populatefromjob": "", "rates": "", @@ -783,6 +788,7 @@ "notes": "", "plate": "", "purchasedate": "", + "readiness": "", "registrationexpires": "", "serviceenddate": "", "servicestartdate": "", @@ -810,6 +816,10 @@ "usage": "", "vehicle": "" }, + "readiness": { + "notready": "", + "ready": "" + }, "status": { "in": "", "inservice": "", @@ -829,17 +839,24 @@ "creating": "", "notconfigured": "", "notfoundsubtitle": "", - "notfoundtitle": "" + "notfoundtitle": "", + "surveycompletetitle": "", + "surveycompletesubtitle": "" }, "fields": { "completedon": "", - "created_at": "" + "created_at": "", + "surveyid": "", + "validuntil": "" }, "labels": { "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "" + "title": "", + "greeting": "", + "intro": "", + "copyright": "" }, "successes": { "created": "", @@ -858,6 +875,7 @@ "labels": { "bodyhrs": "", "dollarsinproduction": "", + "phone": "", "prodhrs": "", "refhrs": "" }, @@ -873,7 +891,9 @@ "productiondollars": "", "productionhours": "", "projectedmonthlysales": "", + "scheduledindate": "", "scheduledintoday": "", + "scheduledoutdate": "", "scheduledouttoday": "" } }, @@ -993,10 +1013,13 @@ }, "labels": { "actions": "", + "active": "", "endmustbeafterstart": "", "flat_rate": "", + "inactive": "", "name": "", "rate_type": "", + "status": "", "straight_time": "" }, "successes": { @@ -1202,6 +1225,31 @@ "updated": "" } }, + "job_lifecycle": { + "columns": { + "duration": "", + "end": "", + "relative_end": "", + "relative_start": "", + "start": "", + "value": "" + }, + "content": { + "current_status_accumulated_time": "", + "data_unavailable": "", + "legend_title": "", + "loading": "", + "not_available": "", + "previous_status_accumulated_time": "", + "title": "", + "title_durations": "", + "title_loading": "", + "title_transitions": "" + }, + "errors": { + "fetch": "Error al obtener los datos del ciclo de vida del trabajo" + } + }, "job_payments": { "buttons": { "goback": "", @@ -1688,6 +1736,7 @@ "ca_gst_all_if_null": "", "calc_repair_days": "", "calc_repair_days_tt": "", + "calc_scheuled_completion": "", "cards": { "customer": "Información al cliente", "damage": "Área de Daño", @@ -1696,6 +1745,7 @@ "estimator": "Estimador", "filehandler": "File Handler", "insurance": "detalles del seguro", + "more": "Más", "notes": "Notas", "parts": "Partes", "totals": "Totales", @@ -1784,6 +1834,7 @@ "override_header": "¿Anular encabezado estimado al importar?", "ownerassociation": "", "parts": "Partes", + "parts_lines": "", "parts_received": "", "parts_tax_rates": "", "partsfilter": "", @@ -1821,6 +1872,7 @@ }, "reconciliationheader": "", "relatedros": "", + "remove_from_ar": "", "returntotals": "", "rosaletotal": "", "sale_additional": "", @@ -1844,6 +1896,7 @@ "total_sales": "", "totals": "", "unvoidnote": "", + "update_scheduled_completion": "", "vehicle_info": "Vehículo", "vehicleassociation": "", "viewallocations": "", @@ -2000,6 +2053,7 @@ "general": "", "insurance": "", "labor": "Labor", + "lifecycle": "", "partssublet": "Piezas / Subarrendamiento", "rates": "", "repairdata": "Datos de reparación", @@ -2046,6 +2100,9 @@ "sentby": "", "typeamessage": "Enviar un mensaje...", "unarchive": "" + }, + "render": { + "conversation_list": "" } }, "notes": { @@ -2521,6 +2578,16 @@ "generate": "" }, "labels": { + "advanced_filters": "", + "advanced_filters_show": "", + "advanced_filters_hide": "", + "advanced_filters_filters": "", + "advanced_filters_sorters": "", + "advanced_filters_filter_field": "", + "advanced_filters_sorter_field": "", + "advanced_filters_sorter_direction": "", + "advanced_filters_filter_operator": "", + "advanced_filters_filter_value": "", "dates": "", "employee": "", "filterson": "", @@ -2548,6 +2615,7 @@ }, "templates": { "anticipated_revenue": "", + "ar_aging": "", "attendance_detail": "", "attendance_employee": "", "attendance_summary": "", @@ -2610,6 +2678,7 @@ "open_orders": "", "open_orders_csr": "", "open_orders_estimator": "", + "open_orders_excel": "", "open_orders_ins_co": "", "open_orders_referral": "", "open_orders_specific_csr": "", @@ -2697,6 +2766,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2711,6 +2781,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", @@ -2881,7 +2952,7 @@ "jobs-intake": "", "jobsavailable": "Empleos disponibles | $t(titles.app)", "jobsdetail": "Trabajo {{ro_number}} | $t(titles.app)", - "jobsdocuments": "Documentos de trabajo {{ro_number}} | $ t (títulos.app)", + "jobsdocuments": "Documentos de trabajo {{ro_number}} | $t(titles.app)", "manageroot": "Casa | $t(titles.app)", "owners": "Todos los propietarios | $t(titles.app)", "owners-detail": "", @@ -2900,6 +2971,10 @@ "shop-csi": "", "shop-templates": "", "shop_vendors": "Vendedores | $t(titles.app)", + "techconsole": "$t(titles.app)", + "techjobclock": "$t(titles.app)", + "techjoblookup": "$t(titles.app)", + "techshiftclock": "$t(titles.app)", "temporarydocs": "", "timetickets": "", "ttapprovals": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index dab607ebc..e2f4705e8 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -99,6 +99,7 @@ }, "audit_trail": { "messages": { + "admin_job_remove_from_ar": "", "admin_jobmarkexported": "", "admin_jobmarkforreexport": "", "admin_jobuninvoice": "", @@ -112,11 +113,11 @@ "jobassignmentchange": "", "jobassignmentremoved": "", "jobchecklist": "", - "jobinvoiced": "", "jobconverted": "", "jobfieldchanged": "", "jobimported": "", "jobinproductionchange": "", + "jobinvoiced": "", "jobioucreated": "", "jobmodifylbradj": "", "jobnoteadded": "", @@ -205,6 +206,7 @@ "entered_total": "", "enteringcreditmemo": "", "federal_tax": "", + "federal_tax_exempt": "", "generatepartslabel": "", "iouexists": "", "local_tax": "", @@ -213,6 +215,7 @@ "new": "", "noneselected": "", "onlycmforinvoiced": "", + "printlabels": "", "retailtotal": "", "savewithdiscrepancy": "", "state_tax": "", @@ -252,7 +255,6 @@ "saving": "" }, "fields": { - "ReceivableCustomField": "", "address1": "", "address2": "", "appt_alt_transport": "", @@ -329,13 +331,13 @@ "md_ded_notes": "", "md_email_cc": "", "md_from_emails": "", + "md_functionality_toggles": { + "parts_queue_toggle": "" + }, "md_hour_split": { "paint": "", "prep": "" }, - "md_functionality_toggles": { - "parts_queue_toggle": "" - }, "md_ins_co": { "city": "", "name": "", @@ -449,6 +451,7 @@ "config": "", "dashboard": "", "rbac": "", + "reportcenter": "", "templates": "", "vendors": "" }, @@ -470,6 +473,7 @@ "editaccess": "" } }, + "ReceivableCustomField": "", "responsibilitycenter": "", "responsibilitycenter_accountdesc": "", "responsibilitycenter_accountitem": "", @@ -743,6 +747,7 @@ "driverinformation": "", "findcontract": "", "findermodal": "", + "insuranceexpired": "", "noteconvertedfrom": "", "populatefromjob": "", "rates": "", @@ -783,6 +788,7 @@ "notes": "", "plate": "", "purchasedate": "", + "readiness": "", "registrationexpires": "", "serviceenddate": "", "servicestartdate": "", @@ -810,6 +816,10 @@ "usage": "", "vehicle": "" }, + "readiness": { + "notready": "", + "ready": "" + }, "status": { "in": "", "inservice": "", @@ -829,17 +839,24 @@ "creating": "", "notconfigured": "", "notfoundsubtitle": "", - "notfoundtitle": "" + "notfoundtitle": "", + "surveycompletetitle": "", + "surveycompletesubtitle": "" }, "fields": { "completedon": "", - "created_at": "" + "created_at": "", + "surveyid": "", + "validuntil": "" }, "labels": { "nologgedinuser": "", "nologgedinuser_sub": "", "noneselected": "", - "title": "" + "title": "", + "greeting": "", + "intro": "", + "copyright": "" }, "successes": { "created": "", @@ -858,6 +875,7 @@ "labels": { "bodyhrs": "", "dollarsinproduction": "", + "phone": "", "prodhrs": "", "refhrs": "" }, @@ -873,7 +891,9 @@ "productiondollars": "", "productionhours": "", "projectedmonthlysales": "", + "scheduledindate": "", "scheduledintoday": "", + "scheduledoutdate": "", "scheduledouttoday": "" } }, @@ -993,10 +1013,13 @@ }, "labels": { "actions": "", + "active": "", "endmustbeafterstart": "", "flat_rate": "", + "inactive": "", "name": "", "rate_type": "", + "status": "", "straight_time": "" }, "successes": { @@ -1202,6 +1225,31 @@ "updated": "" } }, + "job_lifecycle": { + "columns": { + "duration": "", + "end": "", + "relative_end": "", + "relative_start": "", + "start": "", + "value": "" + }, + "content": { + "current_status_accumulated_time": "", + "data_unavailable": "", + "legend_title": "", + "loading": "", + "not_available": "", + "previous_status_accumulated_time": "", + "title": "", + "title_durations": "", + "title_loading": "", + "title_transitions": "" + }, + "errors": { + "fetch": "Erreur lors de l'obtention des données du cycle de vie des tâches" + } + }, "job_payments": { "buttons": { "goback": "", @@ -1688,6 +1736,7 @@ "ca_gst_all_if_null": "", "calc_repair_days": "", "calc_repair_days_tt": "", + "calc_scheuled_completion": "", "cards": { "customer": "Informations client", "damage": "Zone de dommages", @@ -1696,6 +1745,7 @@ "estimator": "Estimateur", "filehandler": "Gestionnaire de fichiers", "insurance": "Détails de l'assurance", + "more": "Plus", "notes": "Remarques", "parts": "les pièces", "totals": "Totaux", @@ -1784,6 +1834,7 @@ "override_header": "Remplacer l'en-tête d'estimation à l'importation?", "ownerassociation": "", "parts": "les pièces", + "parts_lines": "", "parts_received": "", "parts_tax_rates": "", "partsfilter": "", @@ -1821,6 +1872,7 @@ }, "reconciliationheader": "", "relatedros": "", + "remove_from_ar": "", "returntotals": "", "rosaletotal": "", "sale_additional": "", @@ -1844,6 +1896,7 @@ "total_sales": "", "totals": "", "unvoidnote": "", + "update_scheduled_completion": "", "vehicle_info": "Véhicule", "vehicleassociation": "", "viewallocations": "", @@ -2000,6 +2053,7 @@ "general": "", "insurance": "", "labor": "La main d'oeuvre", + "lifecycle": "", "partssublet": "Pièces / Sous-location", "rates": "", "repairdata": "Données de réparation", @@ -2046,6 +2100,9 @@ "sentby": "", "typeamessage": "Envoyer un message...", "unarchive": "" + }, + "render": { + "conversation_list": "" } }, "notes": { @@ -2521,6 +2578,16 @@ "generate": "" }, "labels": { + "advanced_filters": "", + "advanced_filters_show": "", + "advanced_filters_hide": "", + "advanced_filters_filters": "", + "advanced_filters_sorters": "", + "advanced_filters_filter_field": "", + "advanced_filters_sorter_field": "", + "advanced_filters_sorter_direction": "", + "advanced_filters_filter_operator": "", + "advanced_filters_filter_value": "", "dates": "", "employee": "", "filterson": "", @@ -2548,6 +2615,7 @@ }, "templates": { "anticipated_revenue": "", + "ar_aging": "", "attendance_detail": "", "attendance_employee": "", "attendance_summary": "", @@ -2610,6 +2678,7 @@ "open_orders": "", "open_orders_csr": "", "open_orders_estimator": "", + "open_orders_excel": "", "open_orders_ins_co": "", "open_orders_referral": "", "open_orders_specific_csr": "", @@ -2697,6 +2766,7 @@ "efficiencyoverperiod": "", "entries": "", "jobs": "", + "jobscompletednotinvoiced": "", "lastmonth": "", "lastweek": "", "monthlytarget": "", @@ -2711,6 +2781,7 @@ "timetickets": "", "timeticketsemployee": "", "todateactual": "", + "totalhrs": "", "totaloverperiod": "", "weeklyactual": "", "weeklytarget": "", @@ -2881,7 +2952,7 @@ "jobs-intake": "", "jobsavailable": "Emplois disponibles | $t(titles.app)", "jobsdetail": "Travail {{ro_number}} | $t(titles.app)", - "jobsdocuments": "Documents de travail {{ro_number}} | $ t (titres.app)", + "jobsdocuments": "Documents de travail {{ro_number}} | $t(titles.app)", "manageroot": "Accueil | $t(titles.app)", "owners": "Tous les propriétaires | $t(titles.app)", "owners-detail": "", @@ -2900,6 +2971,10 @@ "shop-csi": "", "shop-templates": "", "shop_vendors": "Vendeurs | $t(titles.app)", + "techconsole": "$t(titles.app)", + "techjobclock": "$t(titles.app)", + "techjoblookup": "$t(titles.app)", + "techshiftclock": "$t(titles.app)", "temporarydocs": "", "timetickets": "", "ttapprovals": "", diff --git a/client/src/utils/AuditTrailMappings.js b/client/src/utils/AuditTrailMappings.js index d7098fa2d..eefbb3a11 100644 --- a/client/src/utils/AuditTrailMappings.js +++ b/client/src/utils/AuditTrailMappings.js @@ -1,54 +1,56 @@ import i18n from "i18next"; const AuditTrailMapping = { - alertToggle: (status) => i18n.t("audit_trail.messages.alerttoggle", { status }), + admin_job_remove_from_ar: (status) => + i18n.t("audit_trail.messages.admin_job_remove_from_ar", { status }), + admin_jobfieldchange: (field, value) => + "ADMIN: " + + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + admin_jobmarkexported: () => + i18n.t("audit_trail.messages.admin_jobmarkexported"), + admin_jobmarkforreexport: () => + i18n.t("audit_trail.messages.admin_jobmarkforreexport"), + admin_jobstatuschange: (status) => + "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), + admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"), + admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"), + alertToggle: (status) => + i18n.t("audit_trail.messages.alerttoggle", { status }), appointmentcancel: (lost_sale_reason) => i18n.t("audit_trail.messages.appointmentcancel", { lost_sale_reason }), appointmentinsert: (start) => i18n.t("audit_trail.messages.appointmentinsert", { start }), - jobstatuschange: (status) => - i18n.t("audit_trail.messages.jobstatuschange", { status }), - admin_jobstatuschange: (status) => - "ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }), - jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), - jobimported: () => i18n.t("audit_trail.messages.jobimported"), - jobinvoiced: () => - i18n.t("audit_trail.messages.jobinvoiced"), - jobconverted: (ro_number) => - i18n.t("audit_trail.messages.jobconverted", { ro_number }), - jobfieldchange: (field, value) => - i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), - admin_jobfieldchange: (field, value) => - "ADMIN: " + - i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), - jobspartsorder: (order_number) => - i18n.t("audit_trail.messages.jobspartsorder", { order_number }), - jobspartsreturn: (order_number) => - i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), - jobmodifylbradj: ({ mod_lbr_ty, hours }) => - i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), billposted: (invoice_number) => i18n.t("audit_trail.messages.billposted", { invoice_number }), billupdated: (invoice_number) => i18n.t("audit_trail.messages.billupdated", { invoice_number }), + failedpayment: () => i18n.t("audit_trail.messages.failedpayment"), jobassignmentchange: (operation, name) => i18n.t("audit_trail.messages.jobassignmentchange", { operation, name }), jobassignmentremoved: (operation) => i18n.t("audit_trail.messages.jobassignmentremoved", { operation }), - jobinproductionchange: (inproduction) => - i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), jobchecklist: (type, inproduction, status) => i18n.t("audit_trail.messages.jobchecklist", { type, inproduction, status }), + jobconverted: (ro_number) => + i18n.t("audit_trail.messages.jobconverted", { ro_number }), + jobfieldchange: (field, value) => + i18n.t("audit_trail.messages.jobfieldchanged", { field, value }), + jobimported: () => i18n.t("audit_trail.messages.jobimported"), + jobinproductionchange: (inproduction) => + i18n.t("audit_trail.messages.jobinproductionchange", { inproduction }), + jobinvoiced: () => i18n.t("audit_trail.messages.jobinvoiced"), + jobmodifylbradj: ({ mod_lbr_ty, hours }) => + i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }), jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"), - jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"), - admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"), - admin_jobuninvoice: () => i18n.t("audit_trail.messages.admin_jobuninvoice"), - admin_jobmarkforreexport: () => - i18n.t("audit_trail.messages.admin_jobmarkforreexport"), - admin_jobmarkexported: () => - i18n.t("audit_trail.messages.admin_jobmarkexported"), - failedpayment: () => i18n.t("audit_trail.messages.failedpayment"), + jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"), + jobspartsorder: (order_number) => + i18n.t("audit_trail.messages.jobspartsorder", { order_number }), + jobspartsreturn: (order_number) => + i18n.t("audit_trail.messages.jobspartsreturn", { order_number }), + jobstatuschange: (status) => + i18n.t("audit_trail.messages.jobstatuschange", { status }), + jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"), }; export default AuditTrailMapping; diff --git a/client/src/utils/DateFormatter.jsx b/client/src/utils/DateFormatter.jsx index d034266e3..e8137c54d 100644 --- a/client/src/utils/DateFormatter.jsx +++ b/client/src/utils/DateFormatter.jsx @@ -17,6 +17,9 @@ export function DateTimeFormatter(props) { ) : null; } +export function DateTimeFormatterFunction(date) { + return moment(date).format("MM/DD/YYYY hh:mm a"); +} export function TimeFormatter(props) { return props.children ? moment(props.children).format(props.format ? props.format : "hh:mm a") diff --git a/client/src/utils/DatePickerRanges.js b/client/src/utils/DatePickerRanges.js index aeed206c5..95ee80a45 100644 --- a/client/src/utils/DatePickerRanges.js +++ b/client/src/utils/DatePickerRanges.js @@ -24,4 +24,13 @@ const range = { ], "Last 90 Days": [moment().add(-90, "days"), moment()], }; + +// We are development, lets get crazy +if (process.env.NODE_ENV === "development") { + range["Last year"] = [ + moment().subtract(1, "year"), + moment(), + ]; +} + export default range; diff --git a/client/src/utils/GraphQLClient.js b/client/src/utils/GraphQLClient.js index b6c2c0091..f3758e2e4 100644 --- a/client/src/utils/GraphQLClient.js +++ b/client/src/utils/GraphQLClient.js @@ -12,6 +12,8 @@ import apolloLogger from "apollo-link-logger"; //import axios from "axios"; import { auth } from "../firebase/firebase.utils"; import errorLink from "../graphql/apollo-error-handling"; +import { SentryLink } from "apollo-link-sentry"; + //import { store } from "../redux/store"; const httpLink = new HttpLink({ uri: process.env.REACT_APP_GRAPHQL_ENDPOINT, @@ -105,18 +107,30 @@ const link = split( const authLink = setContext((_, { headers }) => { return ( auth.currentUser && - auth.currentUser.getIdToken().then((token) => { - if (token) { - return { - headers: { - ...headers, - authorization: token ? `Bearer ${token}` : "", - }, - }; - } else { + auth.currentUser + .getIdToken() + .then((token) => { + if (token) { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; + } else { + console.error( + "Authentication error. Unable to add authorization token because it was empty." + ); + return { headers }; + } + }) + .catch((error) => { + console.error( + "Authentication error. Unable to add authorization token.", + error.message + ); return { headers }; - } - }) + }) ); }); @@ -138,8 +152,10 @@ if (process.env.NODE_ENV === "development") { } middlewares.push( - roundTripLink.concat( - retryLink.concat(errorLink.concat(authLink.concat(link))) + new SentryLink().concat( + roundTripLink.concat( + retryLink.concat(errorLink.concat(authLink.concat(link))) + ) ) ); diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 8cb4691fe..000667dbe 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -1,14 +1,16 @@ -import { gql } from "@apollo/client"; +import {gql} from "@apollo/client"; import jsreport from "@jsreport/browser-client"; -import { notification } from "antd"; +import {notification} from "antd"; import axios from "axios"; import _ from "lodash"; -import { auth } from "../firebase/firebase.utils"; -import { setEmailOptions } from "../redux/email/email.actions"; -import { store } from "../redux/store"; +import {auth} from "../firebase/firebase.utils"; +import {setEmailOptions} from "../redux/email/email.actions"; +import {store} from "../redux/store"; import client from "../utils/GraphQLClient"; import cleanAxios from "./CleanAxios"; -import { TemplateList } from "./TemplateConstants"; +import {TemplateList} from "./TemplateConstants"; +import {generateTemplate} from "./graphQLmodifier"; + const server = process.env.REACT_APP_REPORTS_SERVER_URL; jsreport.serverUrl = server; @@ -16,11 +18,11 @@ jsreport.serverUrl = server; const Templates = TemplateList(); export default async function RenderTemplate( - templateObject, - bodyshop, - renderAsHtml = false, - renderAsExcel = false, - renderAsText = false + templateObject, + bodyshop, + renderAsHtml = false, + renderAsExcel = false, + renderAsText = false ) { if (window.jsr3) { jsreport.serverUrl = "https://reports3.test.imex.online/"; @@ -30,41 +32,41 @@ export default async function RenderTemplate( jsreport.headers["Authorization"] = jsrAuth; //Query assets that match the template name. Must be in format <>.query - let { contextData, useShopSpecificTemplate } = await fetchContextData( - templateObject, - jsrAuth + let {contextData, useShopSpecificTemplate} = await fetchContextData( + templateObject, + jsrAuth ); - const { ignoreCustomMargins } = Templates[templateObject.name]; + const {ignoreCustomMargins} = Templates[templateObject.name]; let reportRequest = { template: { name: useShopSpecificTemplate - ? `/${bodyshop.imexshopid}/${templateObject.name}` - : `/${templateObject.name}`, + ? `/${bodyshop.imexshopid}/${templateObject.name}` + : `/${templateObject.name}`, ...(renderAsHtml - ? {} - : { + ? {} + : { recipe: "chrome-pdf", ...(!ignoreCustomMargins && { chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, }), }), - ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}), - ...(renderAsText ? { recipe: "text" } : {}), + ...(renderAsExcel ? {recipe: "html-to-xlsx"} : {}), + ...(renderAsText ? {recipe: "text"} : {}), }, data: { ...contextData, @@ -73,7 +75,10 @@ export default async function RenderTemplate( headerpath: `/${bodyshop.imexshopid}/header.html`, footerpath: `/${bodyshop.imexshopid}/footer.html`, bodyshop: bodyshop, - offset: bodyshop.timezone, //moment().utcOffset(), + filters: templateObject?.filters, + sorters: templateObject?.sorters, + offset: bodyshop.timezone, //dayjs().utcOffset(), + defaultSorters: templateObject?.defaultSorters, }, }; @@ -82,8 +87,8 @@ export default async function RenderTemplate( if (!renderAsHtml) { render.download( - (Templates[templateObject.name] && - Templates[templateObject.name].title) || + (Templates[templateObject.name] && + Templates[templateObject.name].title) || "" ); } else { @@ -97,17 +102,17 @@ export default async function RenderTemplate( ...(!ignoreCustomMargins && { chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, }), }, @@ -121,21 +126,21 @@ export default async function RenderTemplate( resolve({ pdf, filename: - Templates[templateObject.name] && - Templates[templateObject.name].title, + Templates[templateObject.name] && + Templates[templateObject.name].title, html, }); }); } } catch (error) { - notification["error"]({ message: JSON.stringify(error) }); + notification["error"]({message: JSON.stringify(error)}); } } export async function RenderTemplates( - templateObjects, - bodyshop, - renderAsHtml = false + templateObjects, + bodyshop, + renderAsHtml = false ) { //Query assets that match the template name. Must be in format <>.query let unsortedTemplatesAndData = []; @@ -145,17 +150,17 @@ export async function RenderTemplates( templateObjects.forEach((template) => { proms.push( - (async () => { - let { contextData, useShopSpecificTemplate } = await fetchContextData( - template, - jsrAuth - ); - unsortedTemplatesAndData.push({ - templateObject: template, - contextData, - useShopSpecificTemplate, - }); - })() + (async () => { + let {contextData, useShopSpecificTemplate} = await fetchContextData( + template, + jsrAuth + ); + unsortedTemplatesAndData.push({ + templateObject: template, + contextData, + useShopSpecificTemplate, + }); + })() ); }); await Promise.all(proms); @@ -172,8 +177,8 @@ export async function RenderTemplates( unsortedTemplatesAndData.sort(function (a, b) { return ( - templateObjects.findIndex((x) => x.name === a.templateObject.name) - - templateObjects.findIndex((x) => x.name === b.templateObject.name) + templateObjects.findIndex((x) => x.name === a.templateObject.name) - + templateObjects.findIndex((x) => x.name === b.templateObject.name) ); }); const templateAndData = unsortedTemplatesAndData; @@ -183,25 +188,25 @@ export async function RenderTemplates( let reportRequest = { template: { name: rootTemplate.useShopSpecificTemplate - ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` - : `/${rootTemplate.templateObject.name}`, + ? `/${bodyshop.imexshopid}/${rootTemplate.templateObject.name}` + : `/${rootTemplate.templateObject.name}`, ...(renderAsHtml - ? {} - : { + ? {} + : { recipe: "chrome-pdf", chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, }), pdfOperations: [ @@ -218,22 +223,22 @@ export async function RenderTemplates( template: { chrome: { marginTop: - bodyshop.logo_img_path && - bodyshop.logo_img_path.headerMargin && - bodyshop.logo_img_path.headerMargin > 36 - ? bodyshop.logo_img_path.headerMargin - : "36px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.headerMargin && + bodyshop.logo_img_path.headerMargin > 36 + ? bodyshop.logo_img_path.headerMargin + : "36px", marginBottom: - bodyshop.logo_img_path && - bodyshop.logo_img_path.footerMargin && - bodyshop.logo_img_path.footerMargin > 50 - ? bodyshop.logo_img_path.footerMargin - : "50px", + bodyshop.logo_img_path && + bodyshop.logo_img_path.footerMargin && + bodyshop.logo_img_path.footerMargin > 50 + ? bodyshop.logo_img_path.footerMargin + : "50px", }, name: template.useShopSpecificTemplate - ? `/${bodyshop.imexshopid}/${template.templateObject.name}` - : `/${template.templateObject.name}`, - ...(renderAsHtml ? {} : { recipe: "chrome-pdf" }), + ? `/${bodyshop.imexshopid}/${template.templateObject.name}` + : `/${template.templateObject.name}`, + ...(renderAsHtml ? {} : {recipe: "chrome-pdf"}), }, type: "append", @@ -245,8 +250,8 @@ export async function RenderTemplates( }, data: { ...extend( - rootTemplate.contextData, - ...templateAndData.map((temp) => temp.contextData) + rootTemplate.contextData, + ...templateAndData.map((temp) => temp.contextData) ), // ...rootTemplate.templateObject.variables, @@ -266,32 +271,33 @@ export async function RenderTemplates( return render.toString(); } } catch (error) { - notification["error"]({ message: JSON.stringify(error) }); + notification["error"]({message: JSON.stringify(error)}); } } export const GenerateDocument = async ( - template, - messageOptions, - sendType, - jobid + template, + messageOptions, + sendType, + jobid ) => { + const bodyshop = store.getState().user.bodyshop; + if (sendType === "e") { store.dispatch( - setEmailOptions({ - jobid, - messageOptions: { - ...messageOptions, - to: Array.isArray(messageOptions.to) - ? messageOptions.to - : [messageOptions.to], - }, - template, - }) + setEmailOptions({ + jobid, + messageOptions: { + ...messageOptions, + to: Array.isArray(messageOptions.to) + ? messageOptions.to + : [messageOptions.to], + }, + template, + }) ); } else if (sendType === "x") { - console.log("excel"); await RenderTemplate(template, bodyshop, false, true); } else if (sendType === "text") { await RenderTemplate(template, bodyshop, false, false, true); @@ -305,22 +311,74 @@ export const GenerateDocuments = async (templates) => { await RenderTemplates(templates, bodyshop); }; +export const fetchFilterData = async ({name}) => { + try { + const bodyshop = store.getState().user.bodyshop; + const jsrAuth = (await axios.post("/utils/jsr")).data; + jsreport.headers["FirebaseAuthorization"] = + "Bearer " + (await auth.currentUser.getIdToken()); + + const folders = await cleanAxios.get(`${server}/odata/folders`, { + headers: {Authorization: jsrAuth}, + }); + const shopSpecificFolder = folders.data.value.find( + (f) => f.name === bodyshop.imexshopid + ); + + const jsReportFilters = await cleanAxios.get( + `${server}/odata/assets?$filter=name eq '${name}.filters'`, + {headers: {Authorization: jsrAuth}} + ); + console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters); + + let parsedFilterData; + let useShopSpecificTemplate = false; + // let shopSpecificTemplate; + + if (shopSpecificFolder) { + let shopSpecificTemplate = jsReportFilters.data.value.find( + (f) => f?.folder?.shortid === shopSpecificFolder.shortid + ); + if (shopSpecificTemplate) { + useShopSpecificTemplate = true; + parsedFilterData = atob(shopSpecificTemplate.content); + } + } + + if (!parsedFilterData) { + const generalTemplate = jsReportFilters.data.value.find((f) => !f.folder); + useShopSpecificTemplate = false; + if (generalTemplate) parsedFilterData = atob(generalTemplate.content); + } + const data = JSON.parse(parsedFilterData); + return { + data, + useShopSpecificTemplate, + success: true, + } + } catch { + return { + success: false, + } + } +}; + const fetchContextData = async (templateObject, jsrAuth) => { const bodyshop = store.getState().user.bodyshop; jsreport.headers["FirebaseAuthorization"] = - "Bearer " + (await auth.currentUser.getIdToken()); + "Bearer " + (await auth.currentUser.getIdToken()); const folders = await cleanAxios.get(`${server}/odata/folders`, { - headers: { Authorization: jsrAuth }, + headers: {Authorization: jsrAuth}, }); const shopSpecificFolder = folders.data.value.find( - (f) => f.name === bodyshop.imexshopid + (f) => f.name === bodyshop.imexshopid ); const jsReportQueries = await cleanAxios.get( - `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, - { headers: { Authorization: jsrAuth } } + `${server}/odata/assets?$filter=name eq '${templateObject.name}.query'`, + {headers: {Authorization: jsrAuth}} ); let templateQueryToExecute; @@ -329,7 +387,7 @@ const fetchContextData = async (templateObject, jsrAuth) => { if (shopSpecificFolder) { let shopSpecificTemplate = jsReportQueries.data.value.find( - (f) => f?.folder?.shortid === shopSpecificFolder.shortid + (f) => f?.folder?.shortid === shopSpecificFolder.shortid ); if (shopSpecificTemplate) { useShopSpecificTemplate = true; @@ -343,16 +401,35 @@ const fetchContextData = async (templateObject, jsrAuth) => { templateQueryToExecute = atob(generalTemplate.content); } - let contextData = {}; - if (templateQueryToExecute) { - const { data } = await client.query({ - query: gql(templateQueryToExecute), - variables: { ...templateObject.variables }, - }); - contextData = data; + // Commented out for future revision debugging + // console.log('Template Object'); + // console.dir(templateObject); + // console.log('Unmodified Query'); + // console.dir(templateQueryToExecute); + + const hasFilters = templateObject?.filters?.length > 0; + const hasSorters = templateObject?.sorters?.length > 0; + const hasDefaultSorters = templateObject?.defaultSorters?.length > 0; + + // We have no template filters or sorters, so we can just execute the query and return the data + if (!hasFilters && !hasSorters && !hasDefaultSorters) { + let contextData = {}; + if (templateQueryToExecute) { + const {data} = await client.query({ + query: gql(templateQueryToExecute), + variables: {...templateObject.variables}, + }); + contextData = data; + } + + return {contextData, useShopSpecificTemplate}; } - return { contextData, useShopSpecificTemplate }; + return await generateTemplate( + templateQueryToExecute, + templateObject, + useShopSpecificTemplate + ); }; //export const displayTemplateInWindow = (html) => { @@ -389,7 +466,7 @@ const fetchContextData = async (templateObject, jsrAuth) => { function extend(o1, o2, o3) { var result = {}, - obj; + obj; for (var i = 0; i < arguments.length; i++) { obj = arguments[i]; @@ -405,4 +482,4 @@ function extend(o1, o2, o3) { } } return result; -} +} \ No newline at end of file diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index c5e10d712..bb6c23731 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -2026,6 +2026,28 @@ export const TemplateList = (type, context) => { }, group: "customers", }, + open_orders_excel: { + title: i18n.t("reportcenter.templates.open_orders_excel"), + subject: i18n.t("reportcenter.templates.open_orders_excel"), + key: "open_orders_excel", + //idtype: "vendor", + reporttype: "excel", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open"), + }, + group: "jobs", + }, + ar_aging: { + title: i18n.t("reportcenter.templates.ar_aging"), + subject: i18n.t("reportcenter.templates.ar_aging"), + key: "ar_aging", + //idtype: "vendor", + disabled: false, + datedisable: true, + group: "customers", + }, } : {}), ...(!type || type === "courtesycarcontract" @@ -2102,6 +2124,17 @@ export const TemplateList = (type, context) => { // }, } : {}), + ...(!type || type === "messaging" + ? { + conversation_list: { + title: i18n.t("messaging.render.conversation_list"), + description: "", + subject: i18n.t("messaging.render.conversation_list"), + key: "conversation_list", + disabled: false, + }, + } + : {}), ...(!type || type === "vendor" ? { purchases_by_vendor_detailed: { diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js new file mode 100644 index 000000000..3d4079874 --- /dev/null +++ b/client/src/utils/graphQLmodifier.js @@ -0,0 +1,373 @@ +import {Kind, parse, print, visit} from "graphql"; +import client from "./GraphQLClient"; +import {gql} from "@apollo/client"; + +const STRING_OPERATORS = [ + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_like", label: "contains"}, + {value: "_nlike", label: "does not contain"}, + {value: "_ilike", label: "contains case-insensitive"}, + {value: "_nilike", label: "does not contain case-insensitive"} +]; +const NUMBER_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"} +]; +const ORDER_BY_OPERATORS = [ + {value: "asc", label: "ascending"}, + {value: "desc", label: "descending"} +]; + +/** + * Get the available operators for filtering + * @returns {[{label: string, value: string},{label: string, value: string}]} + */ +export function getOrderOperatorsByType() { + return ORDER_BY_OPERATORS; +} + + +/** + * Get the available operators for filtering + * @param type + * @returns {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null]} + */ +export function getWhereOperatorsByType(type = 'string') { + const operators = { + string: STRING_OPERATORS, + number: NUMBER_OPERATORS + }; + return operators[type]; +} + +/* eslint-disable no-loop-func */ + +/** + * Parse a GraphQL query into an AST + * @param query + * @returns {DocumentNode} + */ +export function parseQuery(query) { + return parse(query); +} + +/** + * Print an AST back into a GraphQL query + * @param query + * @returns {string} + */ +export function printQuery(query) { + return print(query); +} + +/** + * Generate a template based on the query and object + * @param templateQueryToExecute + * @param templateObject + * @param useShopSpecificTemplate + * @returns {Promise<{contextData: {}, useShopSpecificTemplate}>} + */ +export async function generateTemplate(templateQueryToExecute, templateObject, useShopSpecificTemplate) { + // Advanced Filtering and Sorting modifications start here + + // Parse the query and apply the filters and sorters + const ast = parseQuery(templateQueryToExecute); + + let filterFields = []; + + if (templateObject?.filters && templateObject?.filters?.length) { + applyFilters(ast, templateObject.filters, filterFields); + wrapFiltersInAnd(ast, filterFields); + } + + if (templateObject?.sorters && templateObject?.sorters?.length) { + applySorters(ast, templateObject.sorters); + } else if (templateObject?.defaultSorters && templateObject?.defaultSorters?.length) { + applySorters(ast, templateObject.defaultSorters); + } + + const finalQuery = printQuery(ast); + + // commented out for future revision debugging + // console.log('Modified Query'); + // console.log(finalQuery); + + let contextData = {}; + if (templateQueryToExecute) { + const {data} = await client.query({ + query: gql(finalQuery), + variables: {...templateObject.variables}, + }); + contextData = data; + } + + return {contextData, useShopSpecificTemplate}; +} + + +/** + * Apply sorters to the AST + * @param ast + * @param sorters + */ +export function applySorters(ast, sorters) { + sorters.forEach((sorter) => { + const fieldPath = sorter.field.split('.'); + visit(ast, { + OperationDefinition: { + enter(node) { + // Loop through each sorter to apply it + // noinspection DuplicatedCode + + let currentSelection = node; // Start with the root operation + + // Navigate down the field path to the correct location + for (let i = 0; i < fieldPath.length - 1; i++) { + let found = false; + visit(currentSelection, { + Field: { + enter(node) { + if (node.name.value === fieldPath[i]) { + currentSelection = node; // Move down to the next level + found = true; + } + } + } + }); + if (!found) break; // Stop if we can't find the next field in the path + } + + // Apply the sorter at the correct level + if (currentSelection) { + const targetFieldName = fieldPath[fieldPath.length - 1]; + let orderByArg = currentSelection.arguments.find(arg => arg.name.value === 'order_by'); + if (!orderByArg) { + orderByArg = { + kind: Kind.ARGUMENT, + name: { kind: Kind.NAME, value: 'order_by' }, + value: { kind: Kind.OBJECT, fields: [] }, + }; + currentSelection.arguments.push(orderByArg); + } + + const sorterField = { + kind: Kind.OBJECT_FIELD, + name: { kind: Kind.NAME, value: targetFieldName }, + value: { kind: Kind.ENUM, value: sorter.direction }, // Adjust if your schema uses a different type for sorting directions + }; + + // Add the new sorter condition + orderByArg.value.fields.push(sorterField); + } + } + } + }); + }); +} + +/** + * Apply filters to the AST + * @param ast + * @param filters + */ +export function applyFilters(ast, filters) { + return visit(ast, { + OperationDefinition: { + enter(node) { + filters.forEach(filter => { + const fieldPath = filter.field.split('.'); + let topLevel = false; + + // Determine if the filter should be applied at the top level + if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { + fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + topLevel = true; + } + + if (topLevel) { + // Construct the filter for a top-level application + const targetFieldName = fieldPath[fieldPath.length - 1]; + 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); + } + } + + }); + } + } + }); +} + +/** + * Get the GraphQL kind for a value + * @param value + * @returns {Kind|Kind.INT} + */ +function getGraphQLKind(value) { + if (typeof value === 'number') { + return value % 1 === 0 ? Kind.INT : Kind.FLOAT; + } else if (typeof value === 'boolean') { + return Kind.BOOLEAN; + } else if (typeof value === 'string') { + return Kind.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 */ \ No newline at end of file diff --git a/client/src/utils/handleBeta.js b/client/src/utils/handleBeta.js new file mode 100644 index 000000000..8a1fba468 --- /dev/null +++ b/client/src/utils/handleBeta.js @@ -0,0 +1,37 @@ +export const BETA_KEY = 'betaSwitchImex'; + +export const checkBeta = () => { + const cookie = document.cookie.split('; ').find(row => row.startsWith(BETA_KEY)); + return cookie ? cookie.split('=')[1] === 'true' : false; +} + + +export const setBeta = (value) => { + const domain = window.location.hostname.split('.').slice(-2).join('.'); + document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`; +} + +export const handleBeta = () => { + // If the current host name does not start with beta or test, then we don't need to do anything. + if (window.location.hostname.startsWith('localhost')) { + console.log('Not on beta or test, so no need to handle beta.'); + return; + } + + const isBeta = checkBeta(); + + const currentHostName = window.location.hostname; + + // Beta is enabled, but the current host name does start with beta. + if (isBeta && !currentHostName.startsWith('beta')) { + const href= `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.replace(href); + } + + // Beta is not enabled, but the current host name does start with beta. + else if (!isBeta && currentHostName.startsWith('beta')) { + const href = `${window.location.protocol}//${currentHostName.replace('beta.', '')}${window.location.pathname}${window.location.search}${window.location.hash}`; + window.location.replace(href); + } +} +export default handleBeta; diff --git a/client/yarn.lock b/client/yarn.lock index ce49d3489..8aeb33e0e 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -53,7 +53,7 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@apollo/client@^3.0.0", "@apollo/client@^3.7.9": +"@apollo/client@^3.7.9": version "3.7.9" resolved "https://registry.npmjs.org/@apollo/client/-/client-3.7.9.tgz" integrity sha512-YnJvrJOVWrp4y/zdNvUaM8q4GuSHCEIecsRDTJhK/veT33P/B7lfqGJ24NeLdKMj8tDEuXYF7V0t+th4+rgC+Q== @@ -96,7 +96,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz" integrity sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.1.0", "@babel/core@^7.12.0", "@babel/core@^7.12.3", "@babel/core@^7.13.0", "@babel/core@^7.16.0", "@babel/core@^7.4.0-0", "@babel/core@^7.8.0", "@babel/core@>=7.11.0": +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.16.0": version "7.18.9" resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz" integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g== @@ -159,6 +159,27 @@ json5 "^2.2.2" semver "^6.3.0" +"@babel/core@^7.8.0": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.21.0.tgz" + integrity sha512-PuxUbxcW6ZYe656yL3EAhpy7qXKq0DmYsrJLpbB8XrsCP9Nm+XCg9XFMb5vIDliPD7+U/+M+QJlH17XOcB7eXA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.21.0" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-module-transforms" "^7.21.0" + "@babel/helpers" "^7.21.0" + "@babel/parser" "^7.21.0" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.0" + "@babel/eslint-parser@^7.16.3": version "7.19.1" resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.19.1.tgz" @@ -729,7 +750,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-flow@^7.14.5", "@babel/plugin-syntax-flow@^7.18.6": +"@babel/plugin-syntax-flow@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz" integrity sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A== @@ -1127,7 +1148,7 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.18.6" -"@babel/plugin-transform-react-jsx@^7.14.9", "@babel/plugin-transform-react-jsx@^7.18.6": +"@babel/plugin-transform-react-jsx@^7.18.6": version "7.18.6" resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.18.6.tgz" integrity sha512-Mz7xMPxoy9kPS/JScj6fJs03TZ/fZ1dJPlMjRAgTaxaS0fUBk8FV/A2rRgfPsVCZqALNwMexD+0Uaf5zlcKPpw== @@ -1568,7 +1589,7 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@craco/craco@^6.0.0", "@craco/craco@^7.0.0": +"@craco/craco@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/@craco/craco/-/craco-7.0.0.tgz" integrity sha512-OyjL9zpURB6Ha1HO62Hlt27Xd7UYJ8DRiBNuE4DBB8Ue0iQ9q/xsv3ze7ROm6gCZqV6I2Gxjnq0EHCCye+4xDQ== @@ -1844,7 +1865,7 @@ "@firebase/util" "1.9.2" tslib "^2.1.0" -"@firebase/app-compat@0.2.3", "@firebase/app-compat@0.x": +"@firebase/app-compat@0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.3.tgz" integrity sha512-sX6rD1KFX6K2CuCnQvc9jZLOgAFZ+sv2jKKahIl4SbTM561D682B8n4Jtx/SgDrvcTVTdb05g4NhZOws9hxYxA== @@ -1855,12 +1876,12 @@ "@firebase/util" "1.9.2" tslib "^2.1.0" -"@firebase/app-types@0.9.0", "@firebase/app-types@0.x": +"@firebase/app-types@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.0.tgz" integrity sha512-AeweANOIo0Mb8GiYm3xhTEBVCmPwTYAu9Hcd2qSkLuga/6+j9b1Jskl5bpiSQWy9eJ/j5pavxj6eYogmnuzm+Q== -"@firebase/app@0.9.3", "@firebase/app@0.x": +"@firebase/app@0.9.3": version "0.9.3" resolved "https://registry.npmjs.org/@firebase/app/-/app-0.9.3.tgz" integrity sha512-G79JUceVDaHRZ4WkA11GyVldVXhdyRJRwWVQFFvAAVfQJLvy2TA6lQjeUn28F6FmeUWxDGwPC30bxCRWq7Op8Q== @@ -2145,7 +2166,7 @@ node-fetch "2.6.7" tslib "^2.1.0" -"@firebase/util@1.9.2", "@firebase/util@1.x": +"@firebase/util@1.9.2": version "1.9.2" resolved "https://registry.npmjs.org/@firebase/util/-/util-1.9.2.tgz" integrity sha512-9l0uMGPGw3GsoD5khjMmYCCcMq/OR/OOSViiWMN+s2Q0pxM+fYzrii1H+r8qC/uoMjSVXomjLZt0vZIyryCqtQ== @@ -3050,7 +3071,7 @@ resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.12", "@types/babel__core@^7.1.9": +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.12": version "7.1.19" resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== @@ -3358,7 +3379,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react@*", "@types/react@^16.8 || ^17.0 || ^18.0", "@types/react@>=16.9.11": +"@types/react@*", "@types/react@>=16.9.11": version "18.0.15" resolved "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz" integrity sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow== @@ -3474,7 +3495,7 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^4.0.0 || ^5.0.0", "@typescript-eslint/eslint-plugin@^5.5.0": +"@typescript-eslint/eslint-plugin@^5.5.0": version "5.54.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.54.0.tgz" integrity sha512-+hSN9BdSr629RF02d7mMtXhAJvDTyCbprNYJKrXETlul/Aml6YZwd90XioVbjejQeHbb3R8Dg0CkRgoJDxo8aw== @@ -3497,7 +3518,7 @@ dependencies: "@typescript-eslint/utils" "5.54.0" -"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@^5.5.0": +"@typescript-eslint/parser@^5.5.0": version "5.54.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.54.0.tgz" integrity sha512-aAVL3Mu2qTi+h/r04WI/5PfNWvO6pdhpeMRWk9R7rEV4mwJNzoWf5CCU5vDKBsPIFQFjEq1xg7XBI2rjiMXQbQ== @@ -3779,11 +3800,6 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: - version "8.8.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - acorn@^7.0.0: version "7.4.1" resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" @@ -3794,6 +3810,11 @@ acorn@^7.1.1: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0: + version "8.8.2" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + address@^1.0.1, address@^1.1.2: version "1.2.2" resolved "https://registry.npmjs.org/address/-/address-1.2.2.tgz" @@ -3841,7 +3862,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.9.1: +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -3851,7 +3872,7 @@ ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.12.6, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0, ajv@^8.8.2: +ajv@^8.0.0, ajv@^8.8.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3861,7 +3882,7 @@ ajv@^8.0.0, ajv@^8.8.0, ajv@^8.8.2: require-from-string "^2.0.2" uri-js "^4.2.2" -ajv@^8.6.0, ajv@>=8: +ajv@^8.6.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -4561,7 +4582,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.21.2, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5, "browserslist@>= 4", "browserslist@>= 4.21.0", browserslist@>=4: +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.20.2, browserslist@^4.21.2, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: version "4.21.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== @@ -5363,7 +5384,7 @@ cuint@^0.2.2: resolved "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz" integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw== -cypress@^10.3.1, "cypress@^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0": +cypress@^10.3.1: version "10.11.0" resolved "https://registry.npmjs.org/cypress/-/cypress-10.11.0.tgz" integrity sha512-lsaE7dprw5DoXM00skni6W5ElVVLGAdRUUdZjX2dYsGjbY/QnpzWZ95Zom1mkGg0hAaO/QVTZoFVS7Jgr/GUPA== @@ -6020,7 +6041,7 @@ enquire.js@^2.1.6: resolved "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz" integrity sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw== -enquirer@^2.3.6, "enquirer@>= 2.3.0 < 3": +enquirer@^2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -6370,7 +6391,7 @@ eslint-webpack-plugin@^3.1.1: normalize-path "^3.0.0" schema-utils "^4.0.0" -eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^7.0.0 || ^8.0.0", "eslint@^7.5.0 || ^8.0.0", eslint@^8.0.0, eslint@^8.1.0, eslint@^8.3.0, "eslint@>= 3.2.1", "eslint@>= 6", eslint@>=5: +eslint@^8.3.0: version "8.35.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz" integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw== @@ -7150,7 +7171,7 @@ graphql-tag@^2.12.6: dependencies: tslib "^2.1.0" -"graphql@^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^14.0.0 || ^15.0.0 || ^16.0.0", "graphql@^15.7.2 || ^16.0.0", graphql@^16.6.0: +graphql@^16.6.0: version "16.6.0" resolved "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz" integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== @@ -7414,7 +7435,7 @@ i18next-browser-languagedetector@^7.0.1: dependencies: "@babel/runtime" "^7.19.4" -i18next@^22.4.10, "i18next@>= 19.0.0": +i18next@^22.4.10: version "22.4.10" resolved "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz" integrity sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA== @@ -8247,7 +8268,7 @@ jest-resolve-dependencies@^27.5.1: jest-regex-util "^27.5.1" jest-snapshot "^27.5.1" -jest-resolve@*, jest-resolve@^27.4.2, jest-resolve@^27.5.1: +jest-resolve@^27.4.2, jest-resolve@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz" integrity sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw== @@ -8457,7 +8478,7 @@ jest-worker@^28.0.2: merge-stream "^2.0.0" supports-color "^8.0.0" -"jest@^27.0.0 || ^28.0.0", jest@^27.4.3: +jest@^27.4.3: version "27.5.1" resolved "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz" integrity sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ== @@ -8724,7 +8745,7 @@ less-loader@^7.3.0: loader-utils "^2.0.0" schema-utils "^3.0.0" -"less@^3.5.0 || ^4.0.0", less@^4.1.1: +less@^4.1.1: version "4.1.3" resolved "https://registry.npmjs.org/less/-/less-4.1.3.tgz" integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA== @@ -9186,7 +9207,7 @@ moment-timezone@^0.5.40, moment-timezone@^0.5.41: dependencies: moment "^2.29.4" -moment@^2.24.0, moment@^2.29.2, moment@^2.29.4, moment@2.x.x: +moment@^2.24.0, moment@^2.29.2, moment@^2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== @@ -10298,15 +10319,6 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -"postcss@^7.0.0 || ^8.0.1", postcss@^8, postcss@^8.0.0, postcss@^8.0.3, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.1.4, postcss@^8.2, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.2.2, postcss@^8.3, postcss@^8.3.5, postcss@^8.4, postcss@^8.4.19, postcss@^8.4.21, postcss@^8.4.4, postcss@^8.4.6, "postcss@>= 8", postcss@>=8, postcss@>=8.0.9: - version "8.4.21" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@^7.0.35: version "7.0.39" resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" @@ -10315,6 +10327,15 @@ postcss@^7.0.35: picocolors "^0.2.1" source-map "^0.6.1" +postcss@^8.0.9, postcss@^8.3.5, postcss@^8.4.19, postcss@^8.4.4: + version "8.4.21" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz" + integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" @@ -10389,7 +10410,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1, prop-types@15.x: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1, prop-types@15.x: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -11119,7 +11140,7 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@*, "react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0", "react-dom@^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", "react-dom@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^15.3.0 || ^16.0.0-alpha", "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.14.0 || ^17 || ^18", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8.0 || ^17.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.5 || ^17.0.0", react-dom@^17.0.2, "react-dom@>= 16.3.0", "react-dom@>= 16.8.0", react-dom@>=15, react-dom@>=15.0.0, react-dom@>=16.0.0, react-dom@>=16.11.0, react-dom@>=16.3.0, react-dom@>=16.9.0, "react-dom@16.x || 17.x": +react-dom@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -11191,7 +11212,7 @@ react-intersection-observer@^9.4.3: resolved "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.4.3.tgz" integrity sha512-WNRqMQvKpupr6MzecAQI0Pj0+JQong307knLP4g/nBex7kYfIaZsPpXaIhKHR+oV8z+goUbH9e10j6lGRnTzlQ== -react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, "react-is@>= 16.8.0": +react-is@^16.10.2, react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -11271,7 +11292,7 @@ react-redux@^8.0.5: react-is "^18.0.0" use-sync-external-store "^1.0.0" -react-refresh@^0.11.0, "react-refresh@>=0.10.0 <1.0.0": +react-refresh@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== @@ -11320,7 +11341,7 @@ react-router@5.3.3: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-scripts@^5.0.0, react-scripts@^5.0.1: +react-scripts@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz" integrity sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ== @@ -11421,7 +11442,7 @@ react-virtualized@^9.22.3: prop-types "^15.7.2" react-lifecycles-compat "^3.0.4" -react@*, "react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0", "react@^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", "react@^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", "react@^15.3.0 || ^16.0.0-alpha", "react@^16.0.0 || ^17.0.0 || ^18.0.0", "react@^16.14.0 || ^17 || ^18", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8.0 || ^17.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.3 || ^17 || ^18", "react@^16.8.5 || ^17.0.0", react@^17.0.2, "react@>= 16", "react@>= 16.3", "react@>= 16.3.0", "react@>= 16.8.0", react@>=15, react@>=15.0.0, react@>=16.0.0, react@>=16.11.0, react@>=16.14.0, react@>=16.3.0, react@>=16.8.0, react@>=16.9.0, "react@15.x || 16.x || 17.x || 18.x", "react@16.x || 17.x", react@17.0.2: +react@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== @@ -11552,7 +11573,7 @@ redux-state-sync@^3.1.4: dependencies: broadcast-channel "^3.1.0" -redux@^4, redux@^4.0.0, redux@^4.0.4, redux@^4.2.1, redux@>4.0.0: +redux@^4.0.0, redux@^4.0.4, redux@^4.2.1: version "4.2.1" resolved "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -11818,7 +11839,7 @@ rollup-plugin-terser@^7.0.0: serialize-javascript "^4.0.0" terser "^5.0.0" -"rollup@^1.20.0 || ^2.0.0", rollup@^1.20.0||^2.0.0, rollup@^2.0.0, rollup@^2.43.1: +rollup@^2.43.1: version "2.79.1" resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz" integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== @@ -11886,7 +11907,7 @@ sass-loader@^12.3.0: klona "^2.0.4" neo-async "^2.6.2" -sass@^1.3.0, sass@^1.58.3: +sass@^1.58.3: version "1.58.3" resolved "https://registry.npmjs.org/sass/-/sass-1.58.3.tgz" integrity sha512-Q7RaEtYf6BflYrQ+buPudKR26/lH+10EmO9bBqbmPh/KeLqv8bjpTNqxe71ocONqXq+jYiCbpPUmQMS+JJPk4A== @@ -12517,7 +12538,7 @@ style-utils@~0.2.0: resolved "https://registry.npmjs.org/style-utils/-/style-utils-0.2.1.tgz" integrity sha512-eKRIfWnUSdBqe2ko+qisUwBSlfWpHru89geRqzmScpDhkPW1ksmE04d//nDcXeF+TVK5cnBG90mMmHgxyxXleQ== -styled-components@^5.3.6, "styled-components@>= 2": +styled-components@^5.3.6: version "5.3.6" resolved "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz" integrity sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg== @@ -12541,7 +12562,7 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" -subscriptions-transport-ws@^0.11.0, "subscriptions-transport-ws@^0.9.0 || ^0.11.0": +subscriptions-transport-ws@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz" integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ== @@ -12896,7 +12917,7 @@ ts-invariant@^0.10.3: dependencies: tslib "^2.1.0" -ts-node@^10.7.0, ts-node@>=9.0.0: +ts-node@^10.7.0: version "10.9.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== @@ -13007,7 +13028,7 @@ type-fest@^0.16.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== -type-fest@^0.20.2, "type-fest@>=0.17.0 <4.0.0": +type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== @@ -13060,11 +13081,6 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -"typescript@^3.2.1 || ^4", "typescript@>= 2.7", typescript@>=2.7, "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@>=3: - version "4.9.5" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" @@ -13370,7 +13386,7 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" -webpack-dev-server@^4.6.0, "webpack-dev-server@3.x || 4.x": +webpack-dev-server@^4.6.0: version "4.11.1" resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz" integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw== @@ -13442,7 +13458,7 @@ webpack-sources@^2.2.0: source-list-map "^2.0.1" source-map "^0.6.1" -"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", "webpack@^4.4.0 || ^5.9.0", "webpack@^4.44.2 || ^5.47.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.64.4, "webpack@>= 4", webpack@>=2, "webpack@>=4.43.0 <6.0.0": +webpack@^5.64.4: version "5.75.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz" integrity sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ== diff --git a/hasura/metadata/functions.yaml b/hasura/metadata/functions.yaml index 3ef10184a..5dd1b8640 100644 --- a/hasura/metadata/functions.yaml +++ b/hasura/metadata/functions.yaml @@ -1,3 +1,6 @@ +- function: + name: jobs_ar_summary + schema: public - function: name: search_bills schema: public diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index d94a97af0..17ccc52a9 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -1388,60 +1388,62 @@ - active: _eq: true columns: - - id - - created_at - - updated_at - bodyshopid - - make - - model - - year - - plate - color - - vin - - fleetnumber - - purchasedate - - servicestartdate - - serviceenddate - - leaseenddate - - status - - nextservicekm - - nextservicedate - - damage - - notes - - fuel - - registrationexpires - - insuranceexpires + - created_at - dailycost + - damage + - fleetnumber + - fuel + - id + - insuranceexpires + - leaseenddate + - make - mileage + - model + - nextservicedate + - nextservicekm + - notes + - plate + - purchasedate + - readiness + - registrationexpires + - serviceenddate + - servicestartdate + - status + - updated_at + - vin + - year select_permissions: - role: user permission: columns: + - bodyshopid + - color + - created_at + - dailycost + - damage + - fleetnumber + - fuel + - id - insuranceexpires - leaseenddate + - make + - mileage + - model - nextservicedate + - nextservicekm + - notes + - plate - purchasedate + - readiness - registrationexpires - serviceenddate - servicestartdate - - dailycost - - fuel - - mileage - - nextservicekm - - color - - damage - - fleetnumber - - make - - model - - notes - - plate - status + - updated_at - vin - year - - created_at - - updated_at - - bodyshopid - - id filter: bodyshop: associations: @@ -1456,31 +1458,32 @@ - role: user permission: columns: + - bodyshopid + - color + - created_at + - dailycost + - damage + - fleetnumber + - fuel + - id - insuranceexpires - leaseenddate + - make + - mileage + - model - nextservicedate + - nextservicekm + - notes + - plate - purchasedate + - readiness - registrationexpires - serviceenddate - servicestartdate - - dailycost - - fuel - - mileage - - nextservicekm - - color - - damage - - fleetnumber - - make - - model - - notes - - plate - status + - updated_at - vin - year - - created_at - - updated_at - - bodyshopid - - id filter: bodyshop: associations: @@ -2020,24 +2023,24 @@ - active: _eq: true columns: - - labor_rates - - percentage - created_at - - updated_at - employeeid - id + - labor_rates + - percentage - teamid + - updated_at select_permissions: - role: user permission: columns: - - labor_rates - - percentage - created_at - - updated_at - employeeid - id + - labor_rates + - percentage - teamid + - updated_at filter: employee_team: bodyshop: @@ -2052,13 +2055,13 @@ - role: user permission: columns: - - labor_rates - - percentage - created_at - - updated_at - employeeid - id + - labor_rates + - percentage - teamid + - updated_at filter: employee_team: bodyshop: @@ -2120,21 +2123,23 @@ _eq: true columns: - active - - name - - created_at - - updated_at - bodyshopid + - created_at - id + - max_load + - name + - updated_at select_permissions: - role: user permission: columns: - active - - name - - created_at - - updated_at - bodyshopid + - created_at - id + - max_load + - name + - updated_at filter: bodyshop: associations: @@ -2150,6 +2155,7 @@ columns: - active - bodyshopid + - max_load - name - updated_at filter: @@ -2417,6 +2423,73 @@ _eq: X-Hasura-User-Id - active: _eq: true +- table: + name: eula_acceptances + schema: public + object_relationships: + - name: eula + using: + foreign_key_constraint_on: eulaid + - name: user + using: + foreign_key_constraint_on: useremail + insert_permissions: + - role: user + permission: + check: + user: + authid: + _eq: X-Hasura-User-Id + columns: + - address + - business_name + - date_accepted + - eulaid + - first_name + - last_name + - phone_number + - useremail + select_permissions: + - role: user + permission: + columns: + - address + - business_name + - first_name + - last_name + - phone_number + - useremail + - created_at + - date_accepted + - updated_at + - eulaid + - id + filter: + user: + authid: + _eq: X-Hasura-User-Id +- table: + name: eulas + schema: public + array_relationships: + - name: eula_acceptances + using: + foreign_key_constraint_on: + column: eulaid + table: + name: eula_acceptances + schema: public + select_permissions: + - role: user + permission: + columns: + - id + - created_at + - updated_at + - effective_date + - end_date + - content + filter: {} - table: name: exportlog schema: public @@ -2600,6 +2673,34 @@ - table: name: ioevents schema: public +- table: + name: job_ar_schema + schema: public + object_relationships: + - name: job + using: + foreign_key_constraint_on: id + select_permissions: + - role: user + permission: + columns: + - id + - ro_number + - clm_total + - total_payments + - balance + - date_invoiced + - shopid + filter: + job: + bodyshop: + associations: + _and: + - active: + _eq: true + - user: + authid: + _eq: X-Hasura-User-Id - table: name: job_conversations schema: public @@ -3739,6 +3840,7 @@ - referral_source - referral_source_extra - regie_number + - remove_from_ar - ro_number - scheduled_completion - scheduled_delivery @@ -4020,6 +4122,7 @@ - referral_source - referral_source_extra - regie_number + - remove_from_ar - ro_number - scheduled_completion - scheduled_delivery @@ -4086,6 +4189,11 @@ - name: job_status_transition definition: enable_manual: true + insert: + columns: '*' + update: + columns: + - status retry_conf: interval_sec: 10 num_retries: 0 @@ -5882,6 +5990,13 @@ table: name: email_audit_trail schema: public + - name: eula_acceptances + using: + foreign_key_constraint_on: + column: useremail + table: + name: eula_acceptances + schema: public - name: exportlogs using: foreign_key_constraint_on: diff --git a/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql new file mode 100644 index 000000000..5e741dcc7 --- /dev/null +++ b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."courtesycars" add column "readiness" text +-- null; diff --git a/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql new file mode 100644 index 000000000..4f5339ede --- /dev/null +++ b/hasura/migrations/1703706449547_alter_table_public_courtesycars_add_column_readiness/up.sql @@ -0,0 +1,2 @@ +alter table "public"."courtesycars" add column "readiness" text + null; diff --git a/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql new file mode 100644 index 000000000..8df675f00 --- /dev/null +++ b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."employee_team_members" add column "max_load" numeric +-- not null default '10000'; diff --git a/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql new file mode 100644 index 000000000..7b49a8dcd --- /dev/null +++ b/hasura/migrations/1704401074280_alter_table_public_employee_team_members_add_column_max_load/up.sql @@ -0,0 +1,2 @@ +alter table "public"."employee_team_members" add column "max_load" numeric + not null default '10000'; diff --git a/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql new file mode 100644 index 000000000..a869e6890 --- /dev/null +++ b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/down.sql @@ -0,0 +1,3 @@ +alter table "public"."employee_team_members" alter column "max_load" set default '10000'::numeric; +alter table "public"."employee_team_members" alter column "max_load" drop not null; +alter table "public"."employee_team_members" add column "max_load" numeric; diff --git a/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql new file mode 100644 index 000000000..18bf59cf3 --- /dev/null +++ b/hasura/migrations/1704403786392_alter_table_public_employee_team_members_drop_column_max_load/up.sql @@ -0,0 +1 @@ +alter table "public"."employee_team_members" drop column "max_load" cascade; diff --git a/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql new file mode 100644 index 000000000..58a0f9b6d --- /dev/null +++ b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."employee_teams" add column "max_load" numeric +-- not null default '10000'; diff --git a/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql new file mode 100644 index 000000000..c55ba33d0 --- /dev/null +++ b/hasura/migrations/1704403846373_alter_table_public_employee_teams_add_column_max_load/up.sql @@ -0,0 +1,2 @@ +alter table "public"."employee_teams" add column "max_load" numeric + not null default '10000'; diff --git a/hasura/migrations/1705522419599_create_table_public_eulas/down.sql b/hasura/migrations/1705522419599_create_table_public_eulas/down.sql new file mode 100644 index 000000000..bea9117e2 --- /dev/null +++ b/hasura/migrations/1705522419599_create_table_public_eulas/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."eulas"; diff --git a/hasura/migrations/1705522419599_create_table_public_eulas/up.sql b/hasura/migrations/1705522419599_create_table_public_eulas/up.sql new file mode 100644 index 000000000..31eaa5f8d --- /dev/null +++ b/hasura/migrations/1705522419599_create_table_public_eulas/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."eulas" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "effective_date" timestamptz NOT NULL, "end_date" timestamptz, "content" text NOT NULL, PRIMARY KEY ("id") ); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_eulas_updated_at" +BEFORE UPDATE ON "public"."eulas" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_eulas_updated_at" ON "public"."eulas" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql new file mode 100644 index 000000000..29d08ee95 --- /dev/null +++ b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."eula_acceptances"; diff --git a/hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql new file mode 100644 index 000000000..18dc09e8e --- /dev/null +++ b/hasura/migrations/1705522869369_create_table_public_eula_acceptances/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "public"."eula_acceptances" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "eulaid" uuid NOT NULL, "date_accepted" timestamptz NOT NULL, "first_name" text NOT NULL, "last_name" text NOT NULL, "address" text NOT NULL, "phone_number" Text NOT NULL, "buisness_name" Text NOT NULL, "useremail" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("eulaid") REFERENCES "public"."eulas"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("useremail") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict); +CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"() +RETURNS TRIGGER AS $$ +DECLARE + _new record; +BEGIN + _new := NEW; + _new."updated_at" = NOW(); + RETURN _new; +END; +$$ LANGUAGE plpgsql; +CREATE TRIGGER "set_public_eula_acceptances_updated_at" +BEFORE UPDATE ON "public"."eula_acceptances" +FOR EACH ROW +EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"(); +COMMENT ON TRIGGER "set_public_eula_acceptances_updated_at" ON "public"."eula_acceptances" +IS 'trigger to set value of column "updated_at" to current timestamp on row update'; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/hasura/migrations/1705693552101_run_sql_migration/down.sql b/hasura/migrations/1705693552101_run_sql_migration/down.sql new file mode 100644 index 000000000..8fd036e68 --- /dev/null +++ b/hasura/migrations/1705693552101_run_sql_migration/down.sql @@ -0,0 +1,33 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF jobs +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.shopid, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- sum(p.amount) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705693552101_run_sql_migration/up.sql b/hasura/migrations/1705693552101_run_sql_migration/up.sql new file mode 100644 index 000000000..56f2ff938 --- /dev/null +++ b/hasura/migrations/1705693552101_run_sql_migration/up.sql @@ -0,0 +1,31 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF jobs + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.shopid, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + sum(p.amount) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql new file mode 100644 index 000000000..1ef08bca9 --- /dev/null +++ b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."job_ar_schema"; diff --git a/hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql new file mode 100644 index 000000000..fe574a140 --- /dev/null +++ b/hasura/migrations/1705693852612_create_table_public_job_ar_schema/up.sql @@ -0,0 +1 @@ +CREATE TABLE "public"."job_ar_schema" ("id" uuid NOT NULL, "ro_number" text, "clm_total" numeric NOT NULL, "total_payments" numeric NOT NULL DEFAULT 0, "balance" numeric NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); diff --git a/hasura/migrations/1705693896379_run_sql_migration/down.sql b/hasura/migrations/1705693896379_run_sql_migration/down.sql new file mode 100644 index 000000000..a180c91a5 --- /dev/null +++ b/hasura/migrations/1705693896379_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- DROP FUNCTION public.jobs_ar_summary; +-- +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- sum(p.amount) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705693896379_run_sql_migration/up.sql b/hasura/migrations/1705693896379_run_sql_migration/up.sql new file mode 100644 index 000000000..48c04c38f --- /dev/null +++ b/hasura/migrations/1705693896379_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +DROP FUNCTION public.jobs_ar_summary; + +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + sum(p.amount) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705694146809_run_sql_migration/down.sql b/hasura/migrations/1705694146809_run_sql_migration/down.sql new file mode 100644 index 000000000..0a84cd97e --- /dev/null +++ b/hasura/migrations/1705694146809_run_sql_migration/down.sql @@ -0,0 +1,32 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705694146809_run_sql_migration/up.sql b/hasura/migrations/1705694146809_run_sql_migration/up.sql new file mode 100644 index 000000000..8b6779a91 --- /dev/null +++ b/hasura/migrations/1705694146809_run_sql_migration/up.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705694176838_run_sql_migration/down.sql b/hasura/migrations/1705694176838_run_sql_migration/down.sql new file mode 100644 index 000000000..0a84cd97e --- /dev/null +++ b/hasura/migrations/1705694176838_run_sql_migration/down.sql @@ -0,0 +1,32 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- p.total_payments, +-- j.clm_total - p.total_payments as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705694176838_run_sql_migration/up.sql b/hasura/migrations/1705694176838_run_sql_migration/up.sql new file mode 100644 index 000000000..8b6779a91 --- /dev/null +++ b/hasura/migrations/1705694176838_run_sql_migration/up.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + p.total_payments, + j.clm_total - p.total_payments as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705696451631_run_sql_migration/down.sql b/hasura/migrations/1705696451631_run_sql_migration/down.sql new file mode 100644 index 000000000..0826360f9 --- /dev/null +++ b/hasura/migrations/1705696451631_run_sql_migration/down.sql @@ -0,0 +1,32 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid ; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705696451631_run_sql_migration/up.sql b/hasura/migrations/1705696451631_run_sql_migration/up.sql new file mode 100644 index 000000000..a78f18383 --- /dev/null +++ b/hasura/migrations/1705696451631_run_sql_migration/up.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid ; + + +END +$function$; diff --git a/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql new file mode 100644 index 000000000..676aeaafd --- /dev/null +++ b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."jobs" add column "remove_from_ar" boolean +-- not null default 'false'; diff --git a/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql new file mode 100644 index 000000000..562618ab9 --- /dev/null +++ b/hasura/migrations/1705696927199_alter_table_public_jobs_add_column_remove_from_ar/up.sql @@ -0,0 +1,2 @@ +alter table "public"."jobs" add column "remove_from_ar" boolean + not null default 'false'; diff --git a/hasura/migrations/1705698426997_run_sql_migration/down.sql b/hasura/migrations/1705698426997_run_sql_migration/down.sql new file mode 100644 index 000000000..c564b5e7c --- /dev/null +++ b/hasura/migrations/1705698426997_run_sql_migration/down.sql @@ -0,0 +1,33 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698426997_run_sql_migration/up.sql b/hasura/migrations/1705698426997_run_sql_migration/up.sql new file mode 100644 index 000000000..f1854fcac --- /dev/null +++ b/hasura/migrations/1705698426997_run_sql_migration/up.sql @@ -0,0 +1,31 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false; + + +END +$function$; diff --git a/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql new file mode 100644 index 000000000..aff5d0afe --- /dev/null +++ b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."job_ar_schema" add column "date_invoiced" timestamptz +-- null; diff --git a/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql new file mode 100644 index 000000000..bd8f477ec --- /dev/null +++ b/hasura/migrations/1705698476261_alter_table_public_job_ar_schema_add_column_date_invoiced/up.sql @@ -0,0 +1,2 @@ +alter table "public"."job_ar_schema" add column "date_invoiced" timestamptz + null; diff --git a/hasura/migrations/1705698534883_run_sql_migration/down.sql b/hasura/migrations/1705698534883_run_sql_migration/down.sql new file mode 100644 index 000000000..a68a79058 --- /dev/null +++ b/hasura/migrations/1705698534883_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- j.date_invoiced, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698534883_run_sql_migration/up.sql b/hasura/migrations/1705698534883_run_sql_migration/up.sql new file mode 100644 index 000000000..9961003f7 --- /dev/null +++ b/hasura/migrations/1705698534883_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + j.date_invoiced, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null; + + +END +$function$; diff --git a/hasura/migrations/1705698593644_run_sql_migration/down.sql b/hasura/migrations/1705698593644_run_sql_migration/down.sql new file mode 100644 index 000000000..c11116ffb --- /dev/null +++ b/hasura/migrations/1705698593644_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698593644_run_sql_migration/up.sql b/hasura/migrations/1705698593644_run_sql_migration/up.sql new file mode 100644 index 000000000..729ee69d9 --- /dev/null +++ b/hasura/migrations/1705698593644_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null; + + +END +$function$; diff --git a/hasura/migrations/1705698876975_run_sql_migration/down.sql b/hasura/migrations/1705698876975_run_sql_migration/down.sql new file mode 100644 index 000000000..190511742 --- /dev/null +++ b/hasura/migrations/1705698876975_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and balance > 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705698876975_run_sql_migration/up.sql b/hasura/migrations/1705698876975_run_sql_migration/up.sql new file mode 100644 index 000000000..4979c0a5e --- /dev/null +++ b/hasura/migrations/1705698876975_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and balance > 0; + + +END +$function$; diff --git a/hasura/migrations/1705700945994_run_sql_migration/down.sql b/hasura/migrations/1705700945994_run_sql_migration/down.sql new file mode 100644 index 000000000..d1ca7dbe0 --- /dev/null +++ b/hasura/migrations/1705700945994_run_sql_migration/down.sql @@ -0,0 +1,34 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705700945994_run_sql_migration/up.sql b/hasura/migrations/1705700945994_run_sql_migration/up.sql new file mode 100644 index 000000000..25c2b3e1e --- /dev/null +++ b/hasura/migrations/1705700945994_run_sql_migration/up.sql @@ -0,0 +1,32 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; + + +END +$function$; diff --git a/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql new file mode 100644 index 000000000..8c4d38542 --- /dev/null +++ b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/down.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" rename column "business_name" to "buisness_name"; diff --git a/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql new file mode 100644 index 000000000..7dc59b9f6 --- /dev/null +++ b/hasura/migrations/1705712927924_alter_table_public_eula_acceptances_alter_column_buisness_name/up.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" rename column "buisness_name" to "business_name"; diff --git a/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql new file mode 100644 index 000000000..08ab66099 --- /dev/null +++ b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/down.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "phone_number" set not null; diff --git a/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql new file mode 100644 index 000000000..de5721802 --- /dev/null +++ b/hasura/migrations/1705715500461_alter_table_public_eula_acceptances_alter_column_phone_number/up.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "phone_number" drop not null; diff --git a/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql new file mode 100644 index 000000000..fa66ebd58 --- /dev/null +++ b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/down.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "address" set not null; diff --git a/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql new file mode 100644 index 000000000..d571c7cf3 --- /dev/null +++ b/hasura/migrations/1705715523486_alter_table_public_eula_acceptances_alter_column_address/up.sql @@ -0,0 +1 @@ +alter table "public"."eula_acceptances" alter column "address" drop not null; diff --git a/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql new file mode 100644 index 000000000..fe15ee344 --- /dev/null +++ b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."job_ar_schema" add column "shopid" uuid +-- null; diff --git a/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql new file mode 100644 index 000000000..349ad38de --- /dev/null +++ b/hasura/migrations/1705952780563_alter_table_public_job_ar_schema_add_column_shopid/up.sql @@ -0,0 +1,2 @@ +alter table "public"."job_ar_schema" add column "shopid" uuid + null; diff --git a/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql new file mode 100644 index 000000000..61b827d98 --- /dev/null +++ b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/down.sql @@ -0,0 +1 @@ +alter table "public"."job_ar_schema" drop constraint "job_ar_schema_id_fkey"; diff --git a/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql new file mode 100644 index 000000000..238c13e33 --- /dev/null +++ b/hasura/migrations/1705952821099_set_fk_public_job_ar_schema_id/up.sql @@ -0,0 +1,5 @@ +alter table "public"."job_ar_schema" + add constraint "job_ar_schema_id_fkey" + foreign key ("id") + references "public"."jobs" + ("id") on update restrict on delete restrict; diff --git a/hasura/migrations/1705952926623_run_sql_migration/down.sql b/hasura/migrations/1705952926623_run_sql_migration/down.sql new file mode 100644 index 000000000..ff68f3148 --- /dev/null +++ b/hasura/migrations/1705952926623_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1705952926623_run_sql_migration/up.sql b/hasura/migrations/1705952926623_run_sql_migration/up.sql new file mode 100644 index 000000000..2ac974bd5 --- /dev/null +++ b/hasura/migrations/1705952926623_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) > 0; + + +END +$function$; diff --git a/hasura/migrations/1706207204357_run_sql_migration/down.sql b/hasura/migrations/1706207204357_run_sql_migration/down.sql new file mode 100644 index 000000000..1deabac64 --- /dev/null +++ b/hasura/migrations/1706207204357_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1706207204357_run_sql_migration/up.sql b/hasura/migrations/1706207204357_run_sql_migration/up.sql new file mode 100644 index 000000000..6a42b8d7a --- /dev/null +++ b/hasura/migrations/1706207204357_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; + + +END +$function$; diff --git a/hasura/migrations/1706207267558_run_sql_migration/down.sql b/hasura/migrations/1706207267558_run_sql_migration/down.sql new file mode 100644 index 000000000..1deabac64 --- /dev/null +++ b/hasura/migrations/1706207267558_run_sql_migration/down.sql @@ -0,0 +1,35 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- CREATE OR REPLACE FUNCTION public.jobs_ar_summary () +-- RETURNS SETOF job_ar_schema +-- LANGUAGE plpgsql +-- STABLE +-- AS $function$ +-- BEGIN +-- +-- RETURN query +-- select +-- j.id, +-- j.ro_number, +-- j.clm_total, +-- coalesce (p.total_payments,0) as total_payments, +-- j.clm_total - coalesce (p.total_payments,0) as balance, +-- j.date_invoiced, +-- j.shopid +-- from +-- jobs j +-- left join ( +-- select +-- p.jobid, +-- coalesce (sum(p.amount),0) as total_payments +-- from +-- payments p +-- group by +-- p.jobid +-- ) p on +-- j.id = p.jobid +-- where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; +-- +-- +-- END +-- $function$; diff --git a/hasura/migrations/1706207267558_run_sql_migration/up.sql b/hasura/migrations/1706207267558_run_sql_migration/up.sql new file mode 100644 index 000000000..6a42b8d7a --- /dev/null +++ b/hasura/migrations/1706207267558_run_sql_migration/up.sql @@ -0,0 +1,33 @@ +CREATE OR REPLACE FUNCTION public.jobs_ar_summary () + RETURNS SETOF job_ar_schema + LANGUAGE plpgsql + STABLE + AS $function$ +BEGIN + + RETURN query +select + j.id, + j.ro_number, + j.clm_total, + coalesce (p.total_payments,0) as total_payments, + j.clm_total - coalesce (p.total_payments,0) as balance, + j.date_invoiced, + j.shopid +from + jobs j +left join ( + select + p.jobid, + coalesce (sum(p.amount),0) as total_payments + from + payments p + group by + p.jobid + ) p on + j.id = p.jobid +where j.remove_from_ar = false and j.date_invoiced is not null and j.clm_total - coalesce (p.total_payments,0) != 0; + + +END +$function$; diff --git a/new_bodyshop_translations.babel b/new_bodyshop_translations.babel new file mode 100644 index 000000000..5409f6ce5 --- /dev/null +++ b/new_bodyshop_translations.babel @@ -0,0 +1,47574 @@ + + + + + generic-json + new_bodyshop_translations.babel + client + + + + + main + + + translation + + + allocations + + + actions + + + assign + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + employee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + appointments + + + actions + + + block + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calculate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cancel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + preview + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reschedule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendreminder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unblock + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + viewjob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + blocking + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + canceling + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + alt_transport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + end + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + start + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + arrivedon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + arrivingjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + blocked + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cancelledappointment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + completingjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dataconsistency + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + expectedjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + expectedprodhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + history + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manualevent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noarrivingjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nocompletingjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nodateselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + priorappointments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reminder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduledfor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + severalerrorsfound + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + smartscheduling + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + suggesteddates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + canceled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + associations + + + actions + + + activate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shopname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + actions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + audit + + + fields + + + cc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contents + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + operation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + subject + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + to + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + useremail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + values + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + audit_trail + + + messages + + + admin_job_remove_from_ar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + admin_jobmarkexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + admin_jobmarkforreexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + admin_jobuninvoice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + admin_jobunvoid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alerttoggle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appointmentcancel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appointmentinsert + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + billposted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + billupdated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + failedpayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobassignmentchange + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobassignmentremoved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobchecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobconverted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobfieldchanged + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobimported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobinproductionchange + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobinvoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobioucreated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobmodifylbradj + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobnoteadded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobnotedeleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobnoteupdated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobspartsorder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobspartsreturn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobstatuschange + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobsupplement + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + billlines + + + actions + + + newline + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + actual_cost + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actual_price + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_center + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_applicable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobline + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + line_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local_tax_applicable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + location + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + quantity + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state_tax_applicable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + deductedfromlbr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + entered + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + from + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + other + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reconciled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unreconciled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + atleastone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + bills + + + actions + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + receive + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + return + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + creating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + existinginventoryline + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exporting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exporting-partner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invalidro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invalidvendor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + allpartslocation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoice_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + is_credit_memo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + is_credit_memo_short + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ro_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vendor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vendorname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + actions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bill_lines + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bill_total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + billcmtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calculatedcreditsnotreceived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creditsnotreceived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creditsreceived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dedfromlbr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleteconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrepancy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrepwithcms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrepwithlbradj + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + editadjwarning + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + entered_total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enteringcreditmemo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_exempt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + generatepartslabel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + iouexists + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local_tax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markforreexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noneselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + onlycmforinvoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + printlabels + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + retailtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + savewithdiscrepancy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state_tax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + subtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalreturns + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + closingperiod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inventoryquantity + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manualinhouse + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unique_invoice_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + bodyshop + + + actions + + + add_task_preset + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addapptcolor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addbucket + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addpartslocation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addpartsrule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addspeedprint + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addtemplate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newlaborrate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newsalestaxcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newstatus + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + testrender + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + loading + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + address1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + address2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appt_alt_transport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appt_colors + + + color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + label + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + appt_length + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + attach_pdf_to_email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bill_allow_post_to_closed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bill_federal_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bill_local_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bill_state_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closingperiod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + country + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailybodytarget + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailypainttarget + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_adjustment_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deliver + + + templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + dms + + + apcontrol + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appostingaccount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cashierid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_journal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + disablebillwip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + disablecontactvehiclecreation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_acctnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_control_override + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_wip_acctnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + generic_customer_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + itc_federal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + itc_local + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + itc_state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mappingname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendmaterialscosting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + srcco + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enforce_class + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enforce_conversion_category + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enforce_conversion_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enforce_referral + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ignoreblockeddays + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inhousevendorid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insurance_vendor_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intake + + + next_contact_hours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + invoice_federal_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoice_local_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoice_state_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jc_hourly_rates + + + mapa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mash + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + last_name_first + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lastnumberworkingdays + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + localmediaserverhttp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + localmediaservernetwork + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + localmediatoken + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logo_img_footer_margin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logo_img_header_margin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logo_img_path + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logo_img_path_height + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logo_img_path_width + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_categories + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_ccc_rates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_classes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_ded_notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_email_cc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_from_emails + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_functionality_toggles + + + parts_queue_toggle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + md_hour_split + + + paint + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prep + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + md_ins_co + + + city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + private + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + street1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + street2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + zip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + md_jobline_presets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_lost_sale_reasons + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_parts_order_comment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_parts_scan + + + expression + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + flags + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + md_payment_types + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_referral_sources + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_tasks_presets + + + hourstype + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + memo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + percent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + messaginglabel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + messagingtext + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noteslabel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notestext + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partslocation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prodtargethrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rbac + + + accounting + + + exportlog + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + receivables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + bills + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + contracts + + + create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + courtesycar + + + create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + csi + + + export + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + page + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + employee_teams + + + page + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + employees + + + page + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + inventory + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + jobs + + + admin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + available-list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + checklist-view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + close + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deliver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list-active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list-all + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list-ready + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partsqueue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + void + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + owners + + + detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + payments + + + enter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + phonebook + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + production + + + board + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + schedule + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + scoreboard + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + shiftclock + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + shop + + + config + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dashboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rbac + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reportcenter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vendors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + temporarydocs + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + timetickets + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + editcommitted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shiftedit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + ttapprovals + + + approve + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + users + + + editaccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + ReceivableCustomField + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenter_accountdesc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenter_accountitem + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenter_accountname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenter_accountnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenter_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenters + + + ap + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ats + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_itc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gst_override + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoiceexemptcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + itemexemptcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la4 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lab + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lad + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lae + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laf + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lam + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + las + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lau + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local_tax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mapa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mash + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pac + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pam + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pan + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pao + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pap + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + par + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pas + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pasl + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refund + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sales_tax_codes + + + code + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + description + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + state_tax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tow + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + schedule_end_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + schedule_start_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shopname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + speedprint + + + id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + label + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + ss_configuration + + + dailyhrslimit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + ssbuckets + + + color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gte + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + label + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + target + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state_tax_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + statuses + + + active_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + additional_board_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_arrived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_bo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_canceled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_completed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_delivered + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_imported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_invoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_ordered + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_quote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_received + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_returned + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_scheduled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + default_void + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + post_production_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pre_production_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_colors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ready_statuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + target_touchtime + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timezone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tt_allow_post_to_invoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tt_enforce_hours_for_tech_console + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + use_fippa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + use_paint_scale_data + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + uselocalmediaserver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + website + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + zip_post + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + 2tiername + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 2tiersetup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 2tiersource + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accountingsetup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accountingtiers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alljobstatuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + allopenjobstatuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + apptcolors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + businessinformation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + checklists + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + csiq + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + customtemplates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + defaultcostsmapping + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + defaultprofitsmapping + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deliverchecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms + + + cdk + + + controllist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + cdk_dealerid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pbs_serialnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + emaillater + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_teams + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employees + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimators + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + filehandlers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insurancecos + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intakechecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobstatuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laborrates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + licensing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_tasks_presets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_to_emails + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + md_to_emails_emails + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + messagingpresets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notemplatesavailable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notespresets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + orderstatuses + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partslocations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partsscan + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + printlater + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + qbo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + qbo_departmentid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + qbo_usa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rbac + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + responsibilitycenters + + + costs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profits + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sales_tax_codes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_accounts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + scheduling + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboardsetup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shopinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + speedprint + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ssbuckets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + systemsettings + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + task-presets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + workingdays + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + centermustexist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + larsplit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + useremailmustexist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + checklist + + + actions + + + printall + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + complete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nochecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + addtoproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + allow_text_message + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + printpack + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removefromproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + completed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + contracts + + + actions + + + changerate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + convertoro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + decodelicense + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + find + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + printcontract + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + senddltoform + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + fetchingjobinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + returning + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectjobandcar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + actax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actualreturn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + agreementnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cc_cardholder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cc_expiry + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cc_num + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cleanupcharge + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + coverage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailyfreekm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailyrate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + damage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + damagewaiver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_addr1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_addr2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_dlexpiry + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_dlnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_dlst + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_dob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_fn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_ln + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_ph1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driver_zip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + excesskmrate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federaltax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fuelin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fuelout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + kmend + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + kmstart + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + length + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + localtax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refuelcharge + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduledreturn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + start + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + statetax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + agreement + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + availablecars + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cardueforservice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + convertform + + + applycleanupcharge + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refuelqty + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + correctdataonform + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dateinpast + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dlexpirebeforereturn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driverinformation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + findcontract + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + findermodal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noteconvertedfrom + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + populatefromjob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + waitingforscan + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + status + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + out + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + returned + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + saved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + courtesycars + + + actions + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + return + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailycost + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + damage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fleetnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fuel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insuranceexpires + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + leaseenddate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + make + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mileage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + model + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nextservicedate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nextservicekm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + plate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchasedate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + readiness + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + registrationexpires + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + serviceenddate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + servicestartdate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + year + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + courtesycar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fuel + + + 12 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 14 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 18 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 34 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 38 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 58 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 78 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + empty + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + full + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + outwith + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + return + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + uniquefleet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + usage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + readiness + + + notready + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ready + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + status + + + in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inservice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + leasereturn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + out + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sold + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + saved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + csi + + + actions + + + activate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + creating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notconfigured + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notfoundsubtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notfoundtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + completedon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created_at + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + nologgedinuser + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nologgedinuser_sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noneselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + submitted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + submittedsub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + dashboard + + + actions + + + addcomponent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + refreshrequired + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updatinglayout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + bodyhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dollarsinproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prodhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + titles + + + labhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + larhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monthlyemployeeefficiency + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monthlyjobcosting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monthlylaborsales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monthlypartssales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monthlyrevenuegraph + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prodhrssummary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productiondollars + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + projectedmonthlysales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduledintoday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduledouttoday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + dms + + + errors + + + alreadyexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + refreshallocations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + documents + + + actions + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + download + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reassign + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectallimages + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectallotherdocuments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + deletes3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleting_cloudinary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + getpresignurl + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insert + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nodocuments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + confirmdelete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + doctype + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newjobid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + openinexplorer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + optimizedimage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reassign_limitexceeded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reassign_limitexceeded_title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + storageexceeded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + storageexceeded_title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + upload + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + upload_limitexceeded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + upload_limitexceeded_title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + uploading + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + usage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edituploaded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insert + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + emails + + + errors + + + notsent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + cc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + from + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + subject + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + to + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + attachments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + documents + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + emailpreview + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + generatingemail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pdfcopywillbeattached + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + preview + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + sent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + employee_teams + + + actions + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newmember + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employeeid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + percentage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + employees + + + actions + + + addvacation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newrate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validationtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + base_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_center + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + external_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + first_name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + flat_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hire_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + last_name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + termination_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + user_email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vacation + + + end + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + length + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + start + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + labels + + + actions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + endmustbeafterstart + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + flat_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inactive + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + straight_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vacationadded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + unique_employee_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + exportlogs + + + fields + + + createdat + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + attempts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + priorsuccesfulexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + general + + + actions + + + add + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calculate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cancel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clear + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + close + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + copied + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + copylink + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleteall + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deselectall + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + login + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + print + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refresh + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + remove + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reset + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + resetpassword + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saveandnew + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectall + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + send + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendbysms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + senderrortosupport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + submit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tryagain + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + viewreleasenotes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + fcm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notfound + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sizelimit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + itemtypes + + + contract + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + actions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + areyousure + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + barcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cancel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clear + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + confirmpassword + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created_at + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + errors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + excel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exceptiontitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + friday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + globalsearch + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + help + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + instanceconflictext + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + instanceconflictitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + item + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + label + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loading + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loadingapp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loadingshop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loggingin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markedexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + message + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + na + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newpassword + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + no + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nointernet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nointernet_sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + none + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + out + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + password + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + passwordresetsuccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + passwordresetsuccess_sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + passwordresetvalidatesuccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + passwordresetvalidatesuccess_sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + passwordsdonotmatch + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + print + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refresh + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reports + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + required + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saturday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + search + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + searchresults + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectdate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendagain + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendby + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + signin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sub_status + + + expired + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successful + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sunday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + text + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + thursday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tuesday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tvmode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unknown + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + username + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + view + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + wednesday + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + yes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + languages + + + english + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + french + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + spanish + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + messages + + + exception + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newversionmessage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newversiontitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noacctfilepath + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nofeatureaccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noshop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notfoundsub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notfoundtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partnernotrunning + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rbacunauth + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unsavedchanges + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unsavedchangespopup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + invalidemail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invalidphone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + required + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + help + + + actions + + + connect + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + codeplacholder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rescuedesc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rescuetitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + intake + + + labels + + + printpack + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + inventory + + + actions + + + addtoinventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addtoro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + consumefrominventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + inserting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + comment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manualinvoicenumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manualvendor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + consumedbyjob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleteconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + frombillinvoicenumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fromvendor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + showall + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + showavailable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inserted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + job_lifecycle + + + columns + + + duration + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + end + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + relative_end + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + relative_start + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + start + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + value + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + content + + + current_status_accumulated_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + data_unavailable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + legend_title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loading + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + not_available + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + previous_status_accumulated_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title_durations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title_loading + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title_transitions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + fetch + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + job_payments + + + buttons + + + goback + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + proceedtopayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refundpayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + notifications + + + error + + + description + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + openingip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + titles + + + amount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dateOfPayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + descriptions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payername + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paymentid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paymentnum + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paymenttype + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refundamount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + transactionid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + joblines + + + actions + + + converttolabor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + creating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + act_price + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ah_detail_line + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + db_price + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lbr_types + + + LA1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LA2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LA3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LA4 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAA + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAB + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAD + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAE + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAF + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAG + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAM + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAR + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAS + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + LAU + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + line_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + line_ind + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + line_no + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + location + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mod_lb_hrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mod_lbr_ty + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + oem_partno + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + op_code_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + part_qty + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + part_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + part_types + + + CCC + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + CCD + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + CCDR + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + CCF + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + CCM + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAA + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAC + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAE + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAG + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAL + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAM + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAN + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAO + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAP + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAR + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PAS + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + PASL + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + profitcenter_labor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profitcenter_part + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_dsmk_m + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_dsmk_p + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_part + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unq_seq + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + adjustmenttobeadded + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + billref + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + convertedtolabor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ioucreated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nostatus + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + presets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validations + + + ahdetailonlyonuserdefinedtypes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hrsrequirediflbrtyp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + requiredifparttype + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + zeropriceexistingpart + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + jobs + + + actions + + + addDocuments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addNote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addtopartsqueue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addtoproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addtoscoreboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + allocate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + autoallocate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + changefilehandler + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + changelaborrate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + changestatus + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + changestimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + convert + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + createiou + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deliver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms + + + addpayer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + createnewcustomer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + findmakemodelcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + getmakes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labels + + + refreshallocations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + post + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refetchmakesmodels + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + usegeneric + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + useselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + dmsautoallocate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exportcustdata + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exportselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + filterpartsonly + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + generatecsi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gotojob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manualnew + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mark + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markasexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markpstexempt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markpstexemptconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + postbills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + printCenter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + recalculate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reconcile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removefromproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + schedule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendcsi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendpartspricechange + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendtodms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sync + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + uninvoice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unvoid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + viewchecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + viewdetail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + addingtoproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cannotintake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exporting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exporting-partner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoicing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noaccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nodamage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nodates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nofinancial + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nojobselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noowner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + novehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partspricechange + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scanimport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalscalc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validationtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + voiding + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + actual_completion + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actual_delivery + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actual_in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + adjustment_bottom_line + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + adjustmenthours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alt_transport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + area_of_damage_impact + + + 01 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 02 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 03 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 04 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 05 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 06 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 07 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 08 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 09 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 10 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 11 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 12 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 13 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 14 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 15 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 16 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 25 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 26 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 27 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 28 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + 34 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + auto_add_ats + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ca_bc_pvrt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ca_customer_gst + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ca_gst_registrant + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + category + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ccc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ccd + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ccdr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ccf + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ccm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cieca_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + claim_total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + class + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clm_no + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clm_total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + comment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + customerowing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_estimated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_invoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_last_contacted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_lost_sale + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_next_contact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_open + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_rentalresp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_repairstarted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_scheduled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_towin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date_void + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ded_amt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ded_note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ded_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + depreciation_taxes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms + + + address + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + amount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + center + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + control_type + + + account_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + cost + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_dms_acctnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_make + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_model + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_model_override + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_unsold + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_wip_acctnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inservicedate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + journal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lines + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payer + + + amount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + control_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + controlnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_acctnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + sale + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sale_dms_acctnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + story + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vinowner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + dms_allocation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + driveable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_body + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_prep + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_refinish + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + est_addr1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + est_co_nm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + est_ct_fn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + est_ct_ln + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + est_ea + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + est_ph1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_addr1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_co_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_co_nm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_co_nm_short + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_ct_fn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_ct_ln + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_ea + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_ph1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intake + + + label + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + max + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + min + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + required + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + invoice_final_note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + kmin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + kmout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + la4 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lab + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labor_rate_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lad + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lae + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laf + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lam + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + las + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lau + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loss_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loss_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + loss_of_use + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lost_sale_reason + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ma2s + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ma3s + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mabl + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + macs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mahw + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mapa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mash + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + matd + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + other_amount_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owner_owing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ea + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ph1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ph2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pac + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pae + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pam + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pan + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pao + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pap + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + par + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_tax_rates + + + prt_discp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_mktyp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_mkupp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_tax_in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_tax_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + partsstatus + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pas + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pay_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phoneshort + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + po_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + policy_no + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ponumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_vars + + + note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + qb_multiple_payers + + + amount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + queued_for_parts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_ats + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_la1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_la2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_la3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_la4 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_laa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lab + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lad + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lae + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_laf + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lam + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_las + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_lau + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_ma2s + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_ma3s + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_mabl + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_macs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_mahw + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_mapa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_mash + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate_matd + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + referral_source_extra + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + referral_source_other + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + referralsource + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + regie_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + repairtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ro_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduled_completion + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduled_delivery + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduled_in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selling_dealer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selling_dealer_contact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + servicecar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + servicing_dealer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + servicing_dealer_contact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + special_coverage_policy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + specialcoveragepolicy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state_tax_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + storage_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_lbr_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_levies_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_paint_mat_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_registration_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_shop_mat_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_str_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_sub_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_tow_rt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + towin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + towing_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unitnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated_at + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + uploaded_by + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + forms + + + admindates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appraiserinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + claiminfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estdates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laborrates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lossinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + other + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + repairdates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheddates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + act_price_ppc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actual_completion_inferred + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actual_delivery_inferred + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + actual_in_inferred + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + additionalpayeroverallocation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + additionaltotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + adjustmentrate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + adjustments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + adminwarning + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + allocations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alreadyaddedtoscoreboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alreadyclosed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appointmentconfirmation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + associationwarning + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + audit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + available + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + availablejobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ca_bc_pvrt + + + days + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + ca_gst_all_if_null + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calc_repair_days + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calc_repair_days_tt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cards + + + customer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + damage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + documents + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + filehandler + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insurance + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + more + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + changeclass + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + checklistcompletedby + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + checklistdocuments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + checklists + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closeconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closejob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closingperiod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + convertedtolabor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_Additional + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_labor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_parts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_sublet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + costs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + create + + + jobinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newowner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newvehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + novehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownerinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicleinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + createiouwarning + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creating_new_job + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deductible + + + stands + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + waived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + deleteconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deletedelivery + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleteintake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deliverchecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + difference + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + diskscan + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms + + + apexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + damageto + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + defaultstory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + disablebillwip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoicedatefuture + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + kmoutnotgreaterthankmin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notallocated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + postingform + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalallocated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + documents + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + documents-images + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + documents-other + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + duplicateconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + emailaudit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employeeassignments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimatelines + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + existing_jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + federal_tax_amt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gpdollars + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gppercent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hrs_claimed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hrs_total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + importnote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intakechecklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + iou + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobcosting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobtotals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labor_rates_subtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laborallocations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labortotals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lines + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + local_tax_amt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mapa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markforreexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mash + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + multipayers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + net_repairs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + othertotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + override_header + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownerassociation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_received + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_tax_rates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partsfilter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partssubletstotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partstotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pimraryamountpayable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + plitooltips + + + billtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calculatedcreditsnotreceived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creditmemos + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creditsnotreceived + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrep1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrep2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrep3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laboradj + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partstotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalreturns + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + ppc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profileadjustments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prt_dsmk_total + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rates_subtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reconciliation + + + billlinestotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + byassoc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + byprice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clear + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discrepancy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + joblinestotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + multipleactprices + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + multiplebilllines + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + multiplebillsforactprice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removedpartsstrikethrough + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + reconciliationheader + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + relatedros + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + remove_from_ar + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + returntotals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rosaletotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sale_additional + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sale_labor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sale_parts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sale_sublet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + savebeforeconversion + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduledinchange + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + specialcoveragepolicy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state_tax_amt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + subletstotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + subtotal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + supplementnote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + suspended + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + suspense + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + threshhold + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total_cost + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total_cust_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total_repairs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + total_sales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unvoidnote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle_info + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicleassociation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + viewallocations + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + voidjob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + voidnote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + addedtoproduction + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + all_deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + converted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creatednoclick + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + duplicated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ioucreated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partsqueue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + savetitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + supplemented + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + voided + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + landing + + + bigfeature + + + subtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + footer + + + company + + + about + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + disclaimers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + privacypolicy + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + io + + + help + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + slogan + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + hero + + + button + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + features + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + managemyshop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pricing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + pricing + + + basic + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + essentials + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + pricingtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pro + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unlimited + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + + + menus + + + currentuser + + + languageselector + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + header + + + accounting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-payables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-receivables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + activejobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alljobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + allpayments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + availablejobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-all + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-contracts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-newcontract + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + customers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dashboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enterbills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + entercardpayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enterpayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + entertimeticket + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export-logs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + help + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + home + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newjob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owners + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts-queue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phonebook + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionlist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + readyjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + recent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reportcenter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rescueme + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + schedule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + search + + + bills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owners + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phonebook + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicles + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + shiftclock + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop_config + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop_csi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop_templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop_vendors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + temporarydocs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ttapprovals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicles + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + jobsactions + + + admin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cancelallappointments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + closejob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deletejob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + duplicate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + duplicatenolines + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newcccontract + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + void + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + jobsdetail + + + claimdetail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + financials + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + general + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insurance + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lifecycle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partssublet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + repairdata + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + profilesidebar + + + profile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shops + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + tech + + + home + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobclockin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobclockout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + joblookup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + login + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + logout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionlist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shiftclockin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + messaging + + + actions + + + link + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + invalidphone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noattachedjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updatinglabel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + addlabel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + archive + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + maxtenimages + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + messaging + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noallowtxt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nojobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + nopush + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phonenumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + presets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + recentonly + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectmedia + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sentby + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + typeamessage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unarchive + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + render + + + conversation_list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + notes + + + actions + + + actions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deletenote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + savetojobnotes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + inserting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + createdby + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + critical + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + private + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + text + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + types + + + customer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + general + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + office + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paint + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + supplement + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + updatedat + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + addtorelatedro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newnoteplaceholder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notetoadd + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + systemnotes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + usernotes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + owner + + + labels + + + noownerinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + owners + + + actions + + + update + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noaccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectexistingornew + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + address + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + allow_text_message + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_addr1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_addr2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_co_nm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ctry + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ea + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_fn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ln + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ph1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_ph2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_st + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_zip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + preferred_contact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + tax_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + forms + + + address + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + create_new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleteconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + existing_owners + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fromclaim + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fromowner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + relatedjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updateowner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + parts + + + actions + + + order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + orderinhouse + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + parts_orders + + + actions + + + backordered + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + receive + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + receivebill + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + associatedbills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + backordering + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + oec + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + act_price + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + backordered_eta + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + backordered_on + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cm_received + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + comments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + db_price + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deliver_by + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_line_id + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + line_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + line_remarks + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lineremarks + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + oem_partno + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + order_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + order_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + orderedby + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + part_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + quantity + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + return + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + allpartsto + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + confirmdelete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + custompercent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inthisorder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + is_quote + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mark_as_received + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newpartsorder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notyetordered + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + oec + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + order_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + orderhistory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_orders + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + print + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + receive + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removefrompartsqueue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + returnpartsorder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sublet_order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + line_updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + received + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + return_created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + payments + + + actions + + + generatepaymentlink + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + exporting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exporting-partner + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inserting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + amount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created_at + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exportedat + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + memo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paymentnum + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + stripeid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + transactionid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + balance + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ca_bc_etf_table + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + customer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + electronicpayment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + external + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + findermodal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + insurance + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markforreexport + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + signup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + smspaymentreminder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalpayments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + markreexported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + stripe + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + phonebook + + + actions + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + adding + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + address1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + address2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + category + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + company + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + country + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fax + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + firstname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lastname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phone1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phone2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + noneselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + onenamerequired + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vendorcategory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + added + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + printcenter + + + appointments + + + appointment_confirmation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + bills + + + inhouse_invoice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + courtesycarcontract + + + courtesy_car_contract + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesy_car_impound + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesy_car_inventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesy_car_terms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + nocontexttype + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + jobs + + + 3rdpartyfields + + + addr1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addr2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + addr3 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + attn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + custgst + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ded_amt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + depreciation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + other + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ponumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refnumber + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sendtype + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + zip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + 3rdpartypayer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ab_proof_of_loss + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appointment_confirmation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + appointment_reminder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + casl_authorization + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + coversheet_landscape + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + coversheet_portrait + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + csi_invitation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + csi_invitation_action + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + diagnostic_authorization + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms_posting_sheet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + envelope_return_address + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimate_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimate_followup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + express_repair_checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + filing_coversheet_landscape + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + filing_coversheet_portrait + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + final_invoice + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fippa_authorization + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + folder_label_multiple + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + glass_express_checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + guarantee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + individual_job_note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoice_customer_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + invoice_total_payable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + iou_form + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_costing_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + key_tag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labels + + + count + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + labels + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + position + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + lag_time_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mechanical_authorization + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mpi_animal_checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mpi_eglass_auth + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mpi_final_acct_sheet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + mpi_final_repair_acct_sheet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paint_grid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_invoice_label_single + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_label_multiple + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_label_single + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_order_confirmation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_order_history + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_return_slip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payment_receipt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payment_request + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments_by_job + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_ro_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_ro_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + qc_sheet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + rental_reservation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ro_totals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ro_with_description + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sgi_certificate_of_repairs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sgi_windshield_auth + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + stolen_recovery_checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sublet_order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + supplement_request + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + thank_you_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + thirdpartypayer + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle_check_in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle_delivery_check + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + window_tag + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + window_tag_sublet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + work_authorization + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + worksheet_by_line_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + worksheet_sorted_by_operation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + worksheet_sorted_by_operation_no_hours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + worksheet_sorted_by_operation_part_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + worksheet_sorted_by_operation_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + groups + + + authorization + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + financial + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + post + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pre + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + worksheet + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + misc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + repairorder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + reportcentermodal + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + speedprint + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + title + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + payments + + + ca_bc_etf_table + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exported_payroll + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + special + + + attendance_detail_csv + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + subjects + + + jobs + + + individual_job_note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_return_slip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sublet_order + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + vendors + + + purchases_by_vendor_detailed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_vendor_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + production + + + actions + + + addcolumns + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bodypriority-clear + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bodypriority-set + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + detailpriority-clear + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + detailpriority-set + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paintpriority-clear + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paintpriority-set + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + remove + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removecolumn + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saveconfig + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + suspend + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unsuspend + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + boardupdate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + settings + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + actual_in + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alert + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alertoff + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + alerton + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ats + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bodyhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bodypriority + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bodyshop + + + labels + + + qbo_departmentid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + qbo_usa + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + cardcolor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cardsettings + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clm_no + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + comment + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + compact + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + detailpriority + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employeeassignments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employeesearch + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_co_nm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobdetail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + laborhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + legend + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ownr_nm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + paintpriority + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + partsstatus + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_note + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refinishhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduled_completion + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectview + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + stickyheader + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sublets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + touchtime + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + viewname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + removed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + profile + + + errors + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + activeshop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + reportcenter + + + actions + + + generate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + dates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + filterson + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + generateasemail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + groups + + + customers + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payroll + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + sales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + key + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + objects + + + appointments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bills + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + csi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exportlogs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_orders + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + vendor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + templates + + + anticipated_revenue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ar_aging + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + attendance_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + attendance_employee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + attendance_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + credits_not_received_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + credits_not_received_date_vendorid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + csi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + customer_list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cycle_time_analysis + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimates_written_converted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimator_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimator_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export_payables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export_payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export_receivables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exported_gsr_by_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + exported_gsr_by_ro_labor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_atp + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_ats + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_category + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_delivery_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_exported_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_make + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_referral + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_by_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + gsr_labor_only + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_closed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_closed_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_closed_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_closed_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_closed_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_open + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_open_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_open_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_open_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_detail_open_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_closed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_closed_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_closed_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_closed_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_closed_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_open + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_open_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_open_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_open_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hours_sold_summary_open_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_costing_ro_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_costing_ro_date_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_costing_ro_date_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_costing_ro_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + job_costing_ro_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs_completed_not_invoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs_invoiced_not_exported + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs_reconcile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs_scheduled_completion + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lag_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lost_sales + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_estimator + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_excel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_referral + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_specific_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + open_orders_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_backorder + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_not_recieved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_not_recieved_vendor + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts_received_not_scheduled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments_by_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments_by_date_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_category + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_category_one + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_csr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_last_name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_repair_status + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_repair_status_one + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_ro + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_target_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_technician + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_by_technician_one + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + production_over_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + psr_by_make + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchase_return_ratio_grouped_by_vendor_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchase_return_ratio_grouped_by_vendor_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_cost_center_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_cost_center_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_date_range_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_date_range_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_vendor_detailed_date_range + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_by_vendor_summary_date_range + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_grouped_by_vendor_detailed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + purchases_grouped_by_vendor_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + returns_grouped_by_vendor_detailed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + returns_grouped_by_vendor_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + schedule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scheduled_parts_list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboard_detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboard_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + supplement_ratio_ins_co + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + thank_you_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets_employee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets_summary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + unclaimed_hrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + void_ros + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + work_in_progress_jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + work_in_progress_labour + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + work_in_progress_payables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + schedule + + + labels + + + atssummary + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employeevacation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + estimators + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ins_co_nm_filter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + intake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manual + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manualevent + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + scoreboard + + + actions + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + adding + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removing + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + bodyhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + painthrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + allemployeetimetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + asoftodaytarget + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + body + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bodycharttitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + calendarperiod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + combinedcharttitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailyactual + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dailytarget + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + efficiencyoverperiod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + entries + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobscompletednotinvoiced + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lastmonth + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lastweek + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + monthlytarget + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + priorweek + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productivestatistics + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productivetimeticketsoverdate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refinish + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + refinishcharttitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + targets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + thismonth + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + thisweek + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timeticketsemployee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + todateactual + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totalhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + totaloverperiod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + weeklyactual + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + weeklytarget + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + workingdays + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + added + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + removed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + tech + + + fields + + + employeeid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + loggedin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notloggedin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + templates + + + errors + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + updated + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + timetickets + + + actions + + + claimtasks + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + enter + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + printemployee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + clockingin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockingout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + creating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noemployeeforuser + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noemployeeforuser_sub + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shiftalreadyclockedon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + actualhrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ciecacode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockoff + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + committed + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_center + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created_by + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + efficiency + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + employee_team + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + flat_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + memo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productivehrs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ro_number + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + alreadyclockedon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ambreak + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + amshift + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockintojob + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleteconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + edit + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + efficiency + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + flat_rate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobhours + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lunch + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pmbreak + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + pmshift + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shift + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shiftalreadyclockedon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + straight_time + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + zeroactualnegativeprod + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + clockedin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockedout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + created + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + clockoffmustbeafterclockon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + clockoffwithoutclockon + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + hoursenteredmorethanavailable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + titles + + + accounting-payables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-receivables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + app + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bc + + + accounting-payables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-payments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + accounting-receivables + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + availablejobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + bills-list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts-create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dashboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export-logs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-admin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-all + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-close + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-deliver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-intake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-ready + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owner-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owners + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts-queue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments-all + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phonebook + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionlist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + schedule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop-csi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop-templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop-vendors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + temporarydocs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ttapprovals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicle-details + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicles + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + bills-list + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts-create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + contracts-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + courtesycars-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dashboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + export-logs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + inventory + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-admin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-all + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-checklist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-close + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-create + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-deliver + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobs-intake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobsavailable + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobsdetail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + jobsdocuments + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + manageroot + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owners + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + owners-detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + parts-queue + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + payments-all + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phonebook + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + productionlist + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + readyjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + resetpassword + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + resetpasswordvalidate + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + schedule + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + scoreboard + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop-csi + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop-templates + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + shop_vendors + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + techconsole + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + techjobclock + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + techjoblookup + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + techshiftclock + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + temporarydocs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + timetickets + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + ttapprovals + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicledetail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + vehicles + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + tt_approvals + + + actions + + + approveselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + user + + + actions + + + changepassword + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + signout + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updateprofile + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + updating + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + authlevel + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + displayname + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + photourl + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + actions + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + changepassword + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + profileinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successess + + + passwordchanged + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + users + + + errors + + + signinerror + + + auth/user-disabled + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + auth/user-not-found + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + auth/wrong-password + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + + + vehicles + + + errors + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + noaccess + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + selectexistingornew + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validation + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + validationtitle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + description + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + notes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + plate_no + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + plate_st + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + trim_color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_bstyle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_color + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_cond + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_engine + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_make_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_makecode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_mldgcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_model_desc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_model_yr + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_options + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_paint_codes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_prod_dt + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_stage + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_tone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_trimcode + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_type + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + v_vin + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + forms + + + detail + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + misc + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + registration + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + deleteconfirm + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + fromvehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + novehinfo + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + relatedjobs + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + updatevehicle + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + delete + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + save + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + vendors + + + actions + + + addtophonebook + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + new + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + newpreferredmake + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + errors + + + deleting + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saving + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + fields + + + active + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + am + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + city + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + cost_center + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + country + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + discount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + display_name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + dmsid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + due_date + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + email + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + favorite + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + lkq + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + make + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + oem + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + phone + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + prompt_discount + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + state + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + street1 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + street2 + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + taxid + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + terms + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + zip + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + labels + + + noneselected + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + preferredmakes + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + search + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + successes + + + deleted + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + saved + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + validation + + + unique_vendor_name + + + + + en-US + false + + + es-ES + false + + + fr-CA + false + + + + + + + + + + + + + + false + + + en-US + + + es-ES + + + fr-CA + + + + + main + + + client/src/translations/en_us/common.json + en-US + + + client/src/translations/es/common.json + es-ES + + + client/src/translations/fr/common.json + fr-CA + + + + + + true + alphabetically + + '%1' + + + + + + + en-US + + tab + namespaced-json + true + + diff --git a/package-lock.json b/package-lock.json index a4e02c21b..889051d11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "node-quickbooks": "^2.0.43", "nodemailer": "^6.9.7", "phone": "^3.1.41", + "rimraf": "^5.0.5", "soap": "^1.0.0", "socket.io": "^4.7.2", "ssh2-sftp-client": "^9.1.0", @@ -55,7 +56,7 @@ "source-map-explorer": "^2.5.2" }, "engines": { - "node": ">=16.0.0", + "node": ">=18.0.0", "npm": ">=8.0.0" } }, @@ -939,6 +940,95 @@ "node": ">=6" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jonkemp/package-utils": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@jonkemp/package-utils/-/package-utils-1.0.8.tgz", @@ -1016,6 +1106,15 @@ "yarn": "^1.22.10" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1897,7 +1996,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "devOptional": true, "engines": { "node": ">=8" } @@ -1906,7 +2004,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2041,8 +2138,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2148,7 +2244,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "devOptional": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2346,7 +2441,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2650,6 +2744,19 @@ "node-fetch": "^2.6.12" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csrf": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", @@ -2954,6 +3061,11 @@ "stream-shift": "^1.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2994,8 +3106,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/enabled": { "version": "2.0.0", @@ -3507,6 +3618,21 @@ } } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -4210,7 +4336,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "devOptional": true, "engines": { "node": ">=8" } @@ -4254,11 +4379,33 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jake": { "version": "10.8.7", "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.7.tgz", @@ -4842,6 +4989,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -5209,6 +5364,37 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -5670,57 +5856,55 @@ } }, "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dependencies": { - "glob": "^7.1.3" + "glob": "^10.3.7" }, "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rndm": { @@ -5869,6 +6053,25 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, "node_modules/shell-quote": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", @@ -5891,6 +6094,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -6313,7 +6527,20 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6335,7 +6562,18 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6523,6 +6761,60 @@ "node": ">=6.0.0" } }, + "node_modules/temp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/text-decoding": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-decoding/-/text-decoding-1.0.0.tgz", @@ -6922,6 +7214,20 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/winston": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", @@ -6981,6 +7287,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 7d2748e0f..3625e8808 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "scripts": { "setup": "rm -rf node_modules && npm i && cd client && rm -rf node_modules && npm i", + "setup:win": "rimraf node_modules && npm i && cd client && rimraf node_modules && npm i", "admin": "cd admin && npm start", "client": "cd client && npm start", "server": "nodemon server.js", @@ -49,6 +50,7 @@ "node-quickbooks": "^2.0.43", "nodemailer": "^6.9.7", "phone": "^3.1.41", + "rimraf": "^5.0.5", "soap": "^1.0.0", "socket.io": "^4.7.2", "ssh2-sftp-client": "^9.1.0", diff --git a/server.js b/server.js index f16d583d5..dbb0d0a5e 100644 --- a/server.js +++ b/server.js @@ -1,294 +1,95 @@ +// Import core modules const express = require("express"); const cors = require("cors"); const bodyParser = require("body-parser"); const path = require("path"); const compression = require("compression"); -const twilio = require("twilio"); -const logger = require("./server/utils/logger"); -const fb = require("./server/firebase/firebase-handler"); const cookieParser = require("cookie-parser"); -const multer = require("multer"); -const upload = multer(); -//var enforce = require("express-sslify"); +const http = require("http"); +const {Server} = require("socket.io"); +// Load environment variables require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`) }); +// Import custom utilities and handlers +const logger = require("./server/utils/logger"); + +// Express app and server setup const app = express(); const port = process.env.PORT || 5000; -//const port = 5000; - -const http = require("http"); const server = http.createServer(app); -const { Server } = require("socket.io"); const io = new Server(server, { - path: "/ws", - cors: { - origin: [ - "https://test.imex.online", - "https://www.test.imex.online", - "http://localhost:3000", - "https://imex.online", - "https://www.imex.online", - ], - methods: ["GET", "POST"], - credentials: true, - exposedHeaders: ["set-cookie"], - }, + path: "/ws", + cors: { + origin: [ + "https://test.imex.online", + "https://www.test.imex.online", + "http://localhost:3000", + "https://imex.online", + "https://www.imex.online", + "https://beta.test.imex.online", + "https://www.beta.test.imex.online", + "https://beta.imex.online", + "https://www.beta.imex.online", + ], + methods: ["GET", "POST"], + credentials: true, + exposedHeaders: ["set-cookie"], + }, }); exports.io = io; + require("./server/web-sockets/web-socket"); -// app.set('trust proxy', true) -// app.use(fb.validateFirebaseIdToken); + + +// Middleware app.use(compression()); app.use(cookieParser()); -app.use(bodyParser.json({ limit: "50mb" })); -app.use(bodyParser.urlencoded({ limit: "50mb", extended: true })); -// app.use(enforce.HTTPS({ trustProtoHeader: true })); -app.use( - cors({ credentials: true, exposedHeaders: ["set-cookie"] }) - // cors({ - // credentials: true, - // origin: [ - // "https://test.imex.online", - // "http://localhost:3000", - // "https://imex.online", - // ], - // }) -); +app.use(bodyParser.json({limit: "50mb"})); +app.use(bodyParser.urlencoded({limit: "50mb", extended: true})); +app.use(cors({credentials: true, exposedHeaders: ["set-cookie"]})); -//Email Based Paths. -var sendEmail = require("./server/email/sendemail.js"); -app.post("/sendemail", fb.validateFirebaseIdToken, sendEmail.sendEmail); -app.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce); - -//Test route to ensure Express is responding. -app.get("/test", async function (req, res) { - const commit = require("child_process").execSync( - "git rev-parse --short HEAD" - ); - // console.log(app.get('trust proxy')); - // console.log("remoteAddress", req.socket.remoteAddress); - // console.log("X-Forwarded-For", req.header('x-forwarded-for')); - logger.log("test-api-status", "DEBUG", "api", { commit }); - // sendEmail.sendServerEmail({ - // subject: `API Check - ${process.env.NODE_ENV}`, - // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, - // }); - sendEmail.sendServerEmail({ - subject: `API Check - ${process.env.NODE_ENV}`, - text: `Server API check has come in.`, - }); - res.status(200).send(`OK - ${commit}`); +// Helper middleware +app.use((req, res, next) => { + req.logger = logger; + next(); }); -//Accounting Qbxml -const accountQbxml = require("./server/accounting/qbxml/qbxml"); -app.post( - "/accounting/qbxml/receivables", - fb.validateFirebaseIdToken, - accountQbxml.receivables -); -app.post( - "/accounting/qbxml/payables", - fb.validateFirebaseIdToken, - accountQbxml.payables -); -app.post( - "/accounting/qbxml/payments", - fb.validateFirebaseIdToken, - accountQbxml.payments -); +// Route groupings +app.use('/', require("./server/routes/miscellaneousRoutes")); +app.use("/notifications", require("./server/routes/notificationsRoutes")); +app.use("/render", require("./server/routes/renderRoutes")); +app.use('/mixdata', require("./server/routes/mixDataRoutes")); +app.use('/accounting', require("./server/routes/accountingRoutes")); +app.use('/qbo', require("./server/routes/qboRoutes")); +app.use('/media', require("./server/routes/mediaRoutes")); +app.use('/sms', require("./server/routes/smsRoutes")); +app.use('/job', require("./server/routes/jobRoutes")); +app.use('/scheduling', require("./server/routes/schedulingRoutes")); +app.use('/utils', require("./server/routes/utilRoutes")); +app.use('/data', require("./server/routes/dataRoutes")); +app.use('/adm', require("./server/routes/adminRoutes")); +app.use('/tech', require("./server/routes/techRoutes")); +app.use('/intellipay', require("./server/routes/intellipayRoutes")); +app.use('/cdk', require("./server/routes/cdkRoutes")); +app.use('/csi', require("./server/routes/csiRoutes")); -//Cloudinary Media Paths -var media = require("./server/media/media"); -app.post( - "/media/sign", - fb.validateFirebaseIdToken, - media.createSignedUploadURL -); -app.post("/media/download", fb.validateFirebaseIdToken, media.downloadFiles); -app.post("/media/rename", fb.validateFirebaseIdToken, media.renameKeys); -app.post("/media/delete", fb.validateFirebaseIdToken, media.deleteFiles); - -//SMS/Twilio Paths -var smsReceive = require("./server/sms/receive"); -app.post( - "/sms/receive", - twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }), - smsReceive.receive -); -var smsSend = require("./server/sms/send"); -app.post("/sms/send", fb.validateFirebaseIdToken, smsSend.send); -var smsStatus = require("./server/sms/status"); -app.post( - "/sms/status", - twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }), - smsStatus.status -); -app.post( - "/sms/markConversationRead", - fb.validateFirebaseIdToken, - smsStatus.markConversationRead -); - -var job = require("./server/job/job"); -app.post("/job/totals", fb.validateFirebaseIdToken, job.totals); -app.post( - "/job/statustransition", - // fb.validateFirebaseIdToken, - job.statustransition -); -app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu); -app.post("/job/costing", fb.validateFirebaseIdToken, job.costing); -app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti); -var partsScan = require("./server/parts-scan/parts-scan"); -app.post("/job/partsscan", fb.validateFirebaseIdToken, partsScan.partsScan); -//Scheduling -var scheduling = require("./server/scheduling/scheduling-job"); -app.post("/scheduling/job", fb.validateFirebaseIdToken, scheduling.job); - -//Handlebars Paths for Email/Report Rendering -// var renderHandlebars = require("./server/render/renderHandlebars"); -// app.post("/render", fb.validateFirebaseIdToken, renderHandlebars.render); -var inlineCss = require("./server/render/inlinecss"); -app.post("/render/inlinecss", fb.validateFirebaseIdToken, inlineCss.inlinecss); - -// app.post( -// "/notifications/send", - -// fb.sendNotification -// ); -app.post("/notifications/subscribe", fb.validateFirebaseIdToken, fb.subscribe); -app.post( - "/notifications/unsubscribe", - fb.validateFirebaseIdToken, - fb.unsubscribe -); -app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser); -app.post("/adm/getuser", fb.validateFirebaseIdToken, fb.getUser); -app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser); -const adm = require("./server/admin/adminops"); -app.post( - "/adm/createassociation", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.createAssociation -); -app.post( - "/adm/createshop", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.createShop -); -app.post( - "/adm/updateshop", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.updateShop -); -app.post( - "/adm/updatecounter", - fb.validateFirebaseIdToken, - fb.validateAdmin, - adm.updateCounter -); - -//Stripe Processing -// var stripe = require("./server/stripe/payment"); -// app.post("/stripe/payment", fb.validateFirebaseIdToken, stripe.payment); -// app.post( -// "/stripe/mobilepayment", -// fb.validateFirebaseIdToken, -// stripe.mobile_payment -// ); - -//Tech Console -var tech = require("./server/tech/tech"); -app.post("/tech/login", fb.validateFirebaseIdToken, tech.techLogin); - -var utils = require("./server/utils/utils"); -app.post("/utils/time", utils.servertime); -app.post("/utils/jsr", fb.validateFirebaseIdToken, utils.jsrAuth); -var qbo = require("./server/accounting/qbo/qbo"); -app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize); -app.get("/qbo/callback", qbo.callback); -app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables); -app.post("/qbo/payables", fb.validateFirebaseIdToken, qbo.payables); -app.post("/qbo/payments", fb.validateFirebaseIdToken, qbo.payments); - -var data = require("./server/data/data"); -app.post("/data/ah", data.autohouse); -app.post("/data/cc", data.claimscorp); -app.post("/record-handler/arms", data.arms); - -var taskHandler = require("./server/tasks/tasks"); -app.post("/taskHandler", fb.validateFirebaseIdToken, taskHandler.taskHandler); - -var mixdataUpload = require("./server/mixdata/mixdata"); - -app.post( - "/mixdata/upload", - fb.validateFirebaseIdToken, - upload.any(), - mixdataUpload.mixdataUpload -); - -var intellipay = require("./server/intellipay/intellipay"); -app.post( - "/intellipay/lightbox_credentials", - fb.validateFirebaseIdToken, - intellipay.lightbox_credentials -); - -app.post( - "/intellipay/payment_refund", - fb.validateFirebaseIdToken, - intellipay.payment_refund -); - -app.post( - "/intellipay/generate_payment_url", - fb.validateFirebaseIdToken, - intellipay.generate_payment_url -); - -app.post( - "/intellipay/postback", - // fb.validateFirebaseIdToken, - intellipay.postback -); - -var ioevent = require("./server/ioevent/ioevent"); -app.post("/ioevent", ioevent.default); -// app.post("/newlog", (req, res) => { -// const { message, type, user, record, object } = req.body; -// logger.log(message, type, user, record, object); -// }); - -var os = require("./server/opensearch/os-handler"); -app.post( - "/opensearch", //fb.validateFirebaseIdToken, - os.handler -); -app.post("/search", fb.validateFirebaseIdToken, os.search); - -var cdkGetMake = require("./server/cdk/cdk-get-makes"); -app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default); - -app.get("/", async function (req, res) { - res.status(200).send("Access Forbidden."); +// Default route for forbidden access +app.get("/", (req, res) => { + res.status(200).send("Access Forbidden."); }); -server.listen(port, (error) => { - if (error) throw error; - logger.log( - `[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`, - "INFO", - "api" - ); -}); +const main = async () => { + await server.listen(port); +} + +// Start server +main() + .then(() => { + logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api"); + }) + .catch((error) => { + logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`, "ERROR", "api", error); + }); \ No newline at end of file diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js index dd63a975d..08fe9ee14 100644 --- a/server/accounting/pbs/pbs-job-export.js +++ b/server/accounting/pbs/pbs-job-export.js @@ -166,7 +166,7 @@ async function CheckForErrors(socket, response) { CdkBase.createLogEvent( socket, "DEBUG", - `Succesful response from DMS. ${response.Message || ""}` + `Successful response from DMS. ${response.Message || ""}` ); } else { CdkBase.createLogEvent( diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 18ab2f8f8..3679db86e 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -18,10 +18,10 @@ const { } = require("./qbo-callback"); const OAuthClient = require("intuit-oauth"); const moment = require("moment-timezone"); -const GraphQLClient = require("graphql-request").GraphQLClient; const findTaxCode = require("../qb-receivables-lines").findTaxCode; exports.default = async (req, res) => { + const oauthClient = new OAuthClient({ clientId: process.env.QBO_CLIENT_ID, clientSecret: process.env.QBO_SECRET, @@ -30,29 +30,31 @@ exports.default = async (req, res) => { redirectUri: process.env.QBO_REDIRECT_URI, logging: true, }); + try { //Fetch the API Access Tokens & Set them for the session. const response = await apiGqlClient.request(queries.GET_QBO_AUTH, { email: req.user.email, }); + const { qbo_realmId } = response.associations[0]; oauthClient.setToken(response.associations[0].qbo_auth); + if (!qbo_realmId) { res.status(401).json({ error: "No company associated." }); return; } + await refreshOauthToken(oauthClient, req); - const BearerToken = req.headers.authorization; const { bills: billsToQuery, elgen } = req.body; - //Query Job Info - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("qbo-payable-create", "DEBUG", req.user.email, billsToQuery); + const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, { diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 13f15e2a6..15418606c 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -51,15 +51,13 @@ exports.default = async (req, res) => { } await refreshOauthToken(oauthClient, req); - const BearerToken = req.headers.authorization; const { payments: paymentsToQuery, elgen } = req.body; - //Query Job Info - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("qbo-payment-create", "DEBUG", req.user.email, paymentsToQuery); + const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_PAYMENTS_FOR_EXPORT, { diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index a09c22d80..fdd7660d8 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -18,8 +18,6 @@ const { const OAuthClient = require("intuit-oauth"); const CreateInvoiceLines = require("../qb-receivables-lines").default; const moment = require("moment-timezone"); - -const GraphQLClient = require("graphql-request").GraphQLClient; const { generateOwnerTier } = require("../qbxml/qbxml-utils"); const { createMultiQbPayerLines } = require("../qb-receivables-lines"); @@ -46,15 +44,14 @@ exports.default = async (req, res) => { await refreshOauthToken(oauthClient, req); - const BearerToken = req.headers.authorization; const { jobIds, elgen } = req.body; //Query Job Info - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("qbo-receivable-create", "DEBUG", req.user.email, jobIds); + const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { diff --git a/server/accounting/qbxml/qbxml-payables.js b/server/accounting/qbxml/qbxml-payables.js index 26f0f2d8a..21cc519b5 100644 --- a/server/accounting/qbxml/qbxml-payables.js +++ b/server/accounting/qbxml/qbxml-payables.js @@ -3,10 +3,11 @@ const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); -var builder = require("xmlbuilder2"); +const builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); const moment = require("moment-timezone"); -const logger = require("../../utils/logger"); +const logger = require('../../utils/logger'); + require("dotenv").config({ path: path.resolve( process.cwd(), @@ -15,14 +16,10 @@ require("dotenv").config({ }); exports.default = async (req, res) => { - const BearerToken = req.headers.authorization; const { bills: billsToQuery } = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; try { logger.log( diff --git a/server/accounting/qbxml/qbxml-payments.js b/server/accounting/qbxml/qbxml-payments.js index 263b1533e..8e563c9ac 100644 --- a/server/accounting/qbxml/qbxml-payments.js +++ b/server/accounting/qbxml/qbxml-payments.js @@ -1,13 +1,12 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); -var builder = require("xmlbuilder2"); +const builder = require("xmlbuilder2"); const moment = require("moment-timezone"); const QbXmlUtils = require("./qbxml-utils"); const QbxmlReceivables = require("./qbxml-receivables"); -const logger = require("../../utils/logger"); +const logger = require('../../utils/logger'); require("dotenv").config({ path: path.resolve( @@ -19,14 +18,10 @@ require("dotenv").config({ const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils; exports.default = async (req, res) => { - const BearerToken = req.headers.authorization; const { payments: paymentsToQuery } = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; try { logger.log( diff --git a/server/accounting/qbxml/qbxml-receivables.js b/server/accounting/qbxml/qbxml-receivables.js index 5489f859e..1d55cee4d 100644 --- a/server/accounting/qbxml/qbxml-receivables.js +++ b/server/accounting/qbxml/qbxml-receivables.js @@ -1,13 +1,12 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const DineroQbFormat = require("../accounting-constants").DineroQbFormat; const queries = require("../../graphql-client/queries"); const Dinero = require("dinero.js"); const moment = require("moment-timezone"); -var builder = require("xmlbuilder2"); +const builder = require("xmlbuilder2"); const QbXmlUtils = require("./qbxml-utils"); -const logger = require("../../utils/logger"); const CreateInvoiceLines = require("../qb-receivables-lines").default; +const logger = require('../../utils/logger'); require("dotenv").config({ path: path.resolve( @@ -20,14 +19,10 @@ Dinero.globalRoundingMode = "HALF_EVEN"; const { generateJobTier, generateOwnerTier, generateSourceTier } = QbXmlUtils; exports.default = async (req, res) => { - const BearerToken = req.headers.authorization; const { jobIds } = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; try { logger.log( diff --git a/server/cdk/cdk-get-makes.js b/server/cdk/cdk-get-makes.js index dc5c59801..0ca36f9e0 100644 --- a/server/cdk/cdk-get-makes.js +++ b/server/cdk/cdk-get-makes.js @@ -5,7 +5,6 @@ require("dotenv").config({ `.env.${process.env.NODE_ENV || "development"}` ), }); -const GraphQLClient = require("graphql-request").GraphQLClient; const soap = require("soap"); const queries = require("../graphql-client/queries"); @@ -34,16 +33,11 @@ const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl"); exports.default = async function ReloadCdkMakes(req, res) { const { bodyshopid, cdk_dealerid } = req.body; try { - const BearerToken = req.headers.authorization; //Query all CDK Models const newList = await GetCdkMakes(req, cdk_dealerid); - //Clear out the existing records - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; const deleteResult = await client .setHeaders({ Authorization: BearerToken }) diff --git a/server/csi/csi.js b/server/csi/csi.js new file mode 100644 index 000000000..819a9ebc7 --- /dev/null +++ b/server/csi/csi.js @@ -0,0 +1,2 @@ +exports.lookup = require("./lookup").default; +exports.submit = require("./submit").default; \ No newline at end of file diff --git a/server/csi/lookup.js b/server/csi/lookup.js new file mode 100644 index 000000000..81a44c2b8 --- /dev/null +++ b/server/csi/lookup.js @@ -0,0 +1,24 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const logger = require("../utils/logger"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); + +const client = require("../graphql-client/graphql-client").client; + +exports.default = async (req, res) => { + try { + logger.log("csi-surveyID-lookup", "DEBUG", "csi", req.body.surveyId, null); + const gql_response = await client.request(queries.QUERY_SURVEY, { + surveyId: req.body.surveyId, + }); + res.status(200).json(gql_response); + } catch (error) { + logger.log("csi-surveyID-lookup", "ERROR", "csi", req.body.surveyId, error); + res.status(400).json(error); + } +}; diff --git a/server/csi/submit.js b/server/csi/submit.js new file mode 100644 index 000000000..a232425ae --- /dev/null +++ b/server/csi/submit.js @@ -0,0 +1,29 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const logger = require("../utils/logger"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); + +const client = require("../graphql-client/graphql-client").client; + +exports.default = async (req, res) => { + try { + logger.log("csi-surveyID-submit", "DEBUG", "csi", req.body.surveyId, null); + const gql_response = await client.request(queries.COMPLETE_SURVEY, { + surveyId: req.body.surveyId, + survey: { + response: req.body.values, + valid: false, + completedon: new Date(), + }, + }); + res.status(200).json(gql_response); + } catch (error) { + logger.log("csi-surveyID-submit", "ERROR", "csi", req.body.surveyId, error); + res.status(400).json(error); + } +}; diff --git a/server/data/claimscorp.js b/server/data/claimscorp.js index 60943aed3..1768f6eb1 100644 --- a/server/data/claimscorp.js +++ b/server/data/claimscorp.js @@ -507,7 +507,7 @@ const CreateRepairOrderTag = (job, errorCallback) => { Body: repairCosts.BodyLaborTotalCost.toFormat(CCDineroFormat), Paint: repairCosts.RefinishLaborTotalCost.toFormat(CCDineroFormat), Prep: Dinero().toFormat(CCDineroFormat), - Frame: Dinero(job.job_totals.rates.laf.total).toFormat(CCDineroFormat), + Frame: repairCosts.FrameLaborTotalCost.toFormat(CCDineroFormat), Mech: repairCosts.MechanicalLaborTotalCost.toFormat(CCDineroFormat), Glass: repairCosts.GlassLaborTotalCost.toFormat(CCDineroFormat), Elec: repairCosts.ElectricalLaborTotalCost.toFormat(CCDineroFormat), diff --git a/server/data/data.js b/server/data/data.js index 077f9f134..18ec4c321 100644 --- a/server/data/data.js +++ b/server/data/data.js @@ -1,3 +1,4 @@ +exports.arms = require("./arms").default; exports.autohouse = require("./autohouse").default; exports.claimscorp = require("./claimscorp").default; -exports.arms = require("./arms").default; \ No newline at end of file +exports.kaizen = require("./kaizen").default; \ No newline at end of file diff --git a/server/data/kaizen.js b/server/data/kaizen.js new file mode 100644 index 000000000..0d3720d25 --- /dev/null +++ b/server/data/kaizen.js @@ -0,0 +1,837 @@ +const path = require("path"); +const queries = require("../graphql-client/queries"); +const Dinero = require("dinero.js"); +const moment = require("moment-timezone"); +var builder = require("xmlbuilder2"); +const _ = require("lodash"); +const logger = require("../utils/logger"); +const fs = require("fs"); +require("dotenv").config({ + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), +}); +let Client = require("ssh2-sftp-client"); + +const client = require("../graphql-client/graphql-client").client; +const { sendServerEmail } = require("../email/sendemail"); +const DineroFormat = "0,0.00"; +const DateFormat = "MM/DD/YYYY"; + +const repairOpCodes = ["OP4", "OP9", "OP10"]; +const replaceOpCodes = ["OP2", "OP5", "OP11", "OP12"]; + +const ftpSetup = { + host: process.env.KAIZEN_HOST, + port: process.env.KAIZEN_PORT, + username: process.env.KAIZEN_USER, + password: process.env.KAIZEN_PASSWORD, + debug: (message, ...data) => logger.log(message, "DEBUG", "api", null, data), + algorithms: { + serverHostKey: [ + "ssh-rsa", + "ssh-dss", + "rsa-sha2-256", + "rsa-sha2-512", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + ], + }, +}; + +exports.default = async (req, res) => { + //Query for the List of Bodyshop Clients. + logger.log("kaizen-start", "DEBUG", "api", null, null); + const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE"]; + + const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, { + imexshopid: kaizenShopsIDs, + }); + + const specificShopIds = req.body.bodyshopIds; // ['uuid] + const { start, end, skipUpload } = req.body; //YYYY-MM-DD + if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) { + res.sendStatus(401); + return; + } + const allxmlsToUpload = []; + const allErrors = []; + try { + for (const bodyshop of specificShopIds + ? bodyshops.filter((b) => specificShopIds.includes(b.id)) + : bodyshops) { + logger.log("kaizen-start-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + const erroredJobs = []; + try { + const { jobs, bodyshops_by_pk } = await client.request( + queries.KAIZEN_QUERY, + { + bodyshopid: bodyshop.id, + start: start + ? moment(start).startOf("hours") + : moment().subtract(2, "hours").startOf("hour"), + ...(end && { end: moment(end).endOf("hours") }), + } + ); + + const kaizenObject = { + DataFeed: { + ShopInfo: { + ShopName: bodyshops_by_pk.shopname, + Jobs: jobs.map((j) => + CreateRepairOrderTag( + { ...j, bodyshop: bodyshops_by_pk }, + function ({ job, error }) { + erroredJobs.push({ job: job, error: error.toString() }); + } + ) + ), + }, + }, + }; + + if (erroredJobs.length > 0) { + logger.log("kaizen-failed-jobs", "ERROR", "api", bodyshop.id, { + count: erroredJobs.length, + jobs: JSON.stringify(erroredJobs.map((j) => j.job.ro_number)), + }); + } + + var ret = builder + .create( + { + // version: "1.0", + // encoding: "UTF-8", + //keepNullNodes: true, + }, + kaizenObject + ) + .end({ allowEmptyTags: true }); + + allxmlsToUpload.push({ + count: kaizenObject.DataFeed.ShopInfo.Jobs.length, + xml: ret, + filename: `${bodyshop.shopname}-${moment().format( + "YYYYMMDDTHHMMss" + )}.xml`, + }); + + logger.log("kaizen-end-shop-extract", "DEBUG", "api", bodyshop.id, { + shopname: bodyshop.shopname, + }); + } catch (error) { + //Error at the shop level. + logger.log("kaizen-error-shop", "ERROR", "api", bodyshop.id, { + ...error, + }); + + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + fatal: true, + errors: [error.toString()], + }); + } finally { + allErrors.push({ + bodyshopid: bodyshop.id, + imexshopid: bodyshop.imexshopid, + shopname: bodyshop.shopname, + errors: erroredJobs.map((ej) => ({ + ro_number: ej.job?.ro_number, + jobid: ej.job?.id, + error: ej.error, + })), + }); + } + } + + if (skipUpload) { + for (const xmlObj of allxmlsToUpload) { + fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml); + } + + res.json(allxmlsToUpload); + sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + return; + } + + let sftp = new Client(); + sftp.on("error", (errors) => + logger.log("kaizen-sftp-error", "ERROR", "api", null, { + ...errors, + }) + ); + try { + //Connect to the FTP and upload all. + + await sftp.connect(ftpSetup); + + for (const xmlObj of allxmlsToUpload) { + logger.log("kaizen-sftp-upload", "DEBUG", "api", null, { + filename: xmlObj.filename, + }); + + const uploadResult = await sftp.put( + Buffer.from(xmlObj.xml), + `/${xmlObj.filename}` + ); + logger.log("kaizen-sftp-upload-result", "DEBUG", "api", null, { + uploadResult, + }); + } + + //***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml + } catch (error) { + logger.log("kaizen-sftp-error", "ERROR", "api", null, { + ...error, + }); + } finally { + sftp.end(); + } + sendServerEmail({ + subject: `Kaizen Report ${moment().format("MM-DD-YY")}`, + text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))} + Uploaded: ${JSON.stringify( + allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })), + null, + 2 + )} + `, + }); + res.sendStatus(200); + } catch (error) { + res.status(200).json(error); + } +}; + +const CreateRepairOrderTag = (job, errorCallback) => { + //Level 2 + + if (!job.job_totals) { + errorCallback({ + jobid: job.id, + job: job, + ro_number: job.ro_number, + error: { toString: () => "No job totals for RO." }, + }); + return {}; + } + + const repairCosts = CreateCosts(job); + + try { + const ret = { + JobID: job.id, + RoNumber: job.ro_number, + JobStatus: job.tlos_ind + ? "Total Loss" + : job.ro_number + ? job.status + : "Estimate", + Customer: { + CompanyName: job.ownr_co_nm?.trim() || "", + FirstName: job.ownr_fn?.trim() || "", + LastName: job.ownr_ln?.trim() || "", + Address1: job.ownr_addr1?.trim() || "", + Address2: job.ownr_addr2?.trim() || "", + City: job.ownr_city?.trim() || "", + State: job.ownr_st?.trim() || "", + Zip: job.ownr_zip?.trim() || "", + }, + Vehicle: { + Year: job.v_model_yr + ? parseInt(job.v_model_yr.match(/\d/g)) + ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10) + : "" + : "", + Make: job.v_make_desc || "", + Model: job.v_model_desc || "", + BodyStyle: job.vehicle?.v_bstyle || "", + Color: job.v_color || "", + VIN: job.v_vin || "", + PlateNo: job.plate_no || "", + }, + InsuranceCompany: job.ins_co_nm || "", + Claim: job.clm_no || "", + Contacts: { + CSR: job.employee_csr_rel + ? `${ + job.employee_csr_rel.last_name + ? job.employee_csr_rel.last_name + : "" + }${job.employee_csr_rel.last_name ? ", " : ""}${ + job.employee_csr_rel.first_name + ? job.employee_csr_rel.first_name + : "" + }` + : "", + Estimator: `${job.est_ct_ln ? job.est_ct_ln : ""}${ + job.est_ct_ln ? ", " : "" + }${job.est_ct_fn ? job.est_ct_fn : ""}`, + }, + Dates: { + DateEstimated: + (job.date_estimated && + moment(job.date_estimated).format(DateFormat)) || + "", + DateOpened: + (job.date_opened && moment(job.date_opened).format(DateFormat)) || "", + DateScheduled: + (job.scheduled_in && + moment(job.scheduled_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateArrived: + (job.actual_in && + moment(job.actual_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateStart: job.date_repairstarted + ? (job.date_repairstarted && + moment(job.date_repairstarted) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "" + : (job.actual_in && + moment(job.actual_in) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateScheduledCompletion: + (job.scheduled_completion && + moment(job.scheduled_completion) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateCompleted: + (job.actual_completion && + moment(job.actual_completion) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateScheduledDelivery: + (job.scheduled_delivery && + moment(job.scheduled_delivery) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateDelivered: + (job.actual_delivery && + moment(job.actual_delivery) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateInvoiced: + (job.date_invoiced && + moment(job.date_invoiced) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + DateExported: + (job.date_exported && + moment(job.date_exported) + .tz(job.bodyshop.timezone) + .format(DateFormat)) || + "", + }, + Sales: { + Labour: { + Aluminum: Dinero(job.job_totals.rates.laa.total).toFormat( + DineroFormat + ), + Body: Dinero(job.job_totals.rates.lab.total).toFormat(DineroFormat), + Diagnostic: Dinero(job.job_totals.rates.lad.total).toFormat( + DineroFormat + ), + Electrical: Dinero(job.job_totals.rates.lae.total).toFormat( + DineroFormat + ), + Frame: Dinero(job.job_totals.rates.laf.total).toFormat(DineroFormat), + Glass: Dinero(job.job_totals.rates.lag.total).toFormat(DineroFormat), + Mechanical: Dinero(job.job_totals.rates.lam.total).toFormat( + DineroFormat + ), + OtherLabour: Dinero(job.job_totals.rates.la1.total) + .add(Dinero(job.job_totals.rates.la2.total)) + .add(Dinero(job.job_totals.rates.la3.total)) + .add(Dinero(job.job_totals.rates.la4.total)) + .add(Dinero(job.job_totals.rates.lau.total)) + .toFormat(DineroFormat), + Refinish: Dinero(job.job_totals.rates.lar.total).toFormat( + DineroFormat + ), + Structural: Dinero(job.job_totals.rates.las.total).toFormat( + DineroFormat + ), + }, + Materials: { + Body: Dinero(job.job_totals.rates.mash.total).toFormat(DineroFormat), + Refinish: Dinero(job.job_totals.rates.mapa.total).toFormat( + DineroFormat + ), + }, + Parts: { + Aftermarket: Dinero( + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total + ).toFormat(DineroFormat), + LKQ: Dinero( + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total + ).toFormat(DineroFormat), + OEM: Dinero( + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAP && + job.job_totals.parts.parts.list.PAP.total + ) + ) + .toFormat(DineroFormat), + OtherParts: Dinero( + job.job_totals.parts.parts.list.PAO && + job.job_totals.parts.parts.list.PAO.total + ).toFormat(DineroFormat), + Reconditioned: Dinero( + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total + ).toFormat(DineroFormat), + TotalParts: Dinero( + job.job_totals.parts.parts.list.PAA && + job.job_totals.parts.parts.list.PAA.total + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAL && + job.job_totals.parts.parts.list.PAL.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAN && + job.job_totals.parts.parts.list.PAN.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAO && + job.job_totals.parts.parts.list.PAO.total + ) + ) + .add( + Dinero( + job.job_totals.parts.parts.list.PAM && + job.job_totals.parts.parts.list.PAM.total + ) + ) + .toFormat(DineroFormat), + }, + OtherSales: Dinero(job.job_totals.additional.storage).toFormat( + DineroFormat + ), + Sublet: Dinero(job.job_totals.parts.sublets.total).toFormat( + DineroFormat + ), + Towing: Dinero(job.job_totals.additional.towing).toFormat(DineroFormat), + ATS: + job.job_totals.additional.additionalCostItems.includes( + "ATS Amount" + ) === true + ? Dinero( + job.job_totals.additional.additionalCostItems[ + job.job_totals.additional.additionalCostItems.indexOf( + "ATS Amount" + ) + ].total + ).toFormat(DineroFormat) + : Dinero().toFormat(DineroFormat), + SaleSubtotal: Dinero(job.job_totals.totals.subtotal).toFormat( + DineroFormat + ), + Tax: Dinero(job.job_totals.totals.local_tax) + .add(Dinero(job.job_totals.totals.state_tax)) + .add(Dinero(job.job_totals.totals.federal_tax)) + .add(Dinero(job.job_totals.additional.pvrt)) + .toFormat(DineroFormat), + SaleTotal: Dinero(job.job_totals.totals.total_repairs).toFormat( + DineroFormat + ), + }, + SaleHours: { + Aluminum: job.job_totals.rates.laa.hours.toFixed(2), + Body: job.job_totals.rates.lab.hours.toFixed(2), + Diagnostic: job.job_totals.rates.lad.hours.toFixed(2), + Electrical: job.job_totals.rates.lae.hours.toFixed(2), + Frame: job.job_totals.rates.laf.hours.toFixed(2), + Glass: job.job_totals.rates.lag.hours.toFixed(2), + Mechanical: job.job_totals.rates.lam.hours.toFixed(2), + Other: ( + job.job_totals.rates.la1.hours + + job.job_totals.rates.la2.hours + + job.job_totals.rates.la3.hours + + job.job_totals.rates.la4.hours + + job.job_totals.rates.lau.hours + ).toFixed(2), + Refinish: job.job_totals.rates.lar.hours.toFixed(2), + Structural: job.job_totals.rates.las.hours.toFixed(2), + TotalHours: job.joblines + .reduce((acc, val) => acc + val.mod_lb_hrs, 0) + .toFixed(2), + }, + Costs: { + Labour: { + Aluminum: repairCosts.AluminumLabourTotalCost.toFormat(DineroFormat), + Body: repairCosts.BodyLabourTotalCost.toFormat(DineroFormat), + Diagnostic: + repairCosts.DiagnosticLabourTotalCost.toFormat(DineroFormat), + Electrical: + repairCosts.ElectricalLabourTotalCost.toFormat(DineroFormat), + Frame: repairCosts.FrameLabourTotalCost.toFormat(DineroFormat), + Glass: repairCosts.GlassLabourTotalCost.toFormat(DineroFormat), + Mechancial: + repairCosts.MechanicalLabourTotalCost.toFormat(DineroFormat), + OtherLabour: repairCosts.LabourMiscTotalCost.toFormat(DineroFormat), + Refinish: repairCosts.RefinishLabourTotalCost.toFormat(DineroFormat), + Structural: + repairCosts.StructuralLabourTotalCost.toFormat(DineroFormat), + TotalLabour: repairCosts.LabourTotalCost.toFormat(DineroFormat), + }, + Materials: { + Body: repairCosts.BMTotalCost.toFormat(DineroFormat), + Refinish: repairCosts.PMTotalCost.toFormat(DineroFormat), + }, + Parts: { + Aftermarket: repairCosts.PartsAMCost.toFormat(DineroFormat), + LKQ: repairCosts.PartsRecycledCost.toFormat(DineroFormat), + OEM: repairCosts.PartsOemCost.toFormat(DineroFormat), + OtherCost: repairCosts.PartsOtherCost.toFormat(DineroFormat), + Reconditioned: + repairCosts.PartsReconditionedCost.toFormat(DineroFormat), + TotalParts: repairCosts.PartsAMCost.add(repairCosts.PartsRecycledCost) + .add(repairCosts.PartsReconditionedCost) + .add(repairCosts.PartsOemCost) + .add(repairCosts.PartsOtherCost) + .toFormat(DineroFormat), + }, + Sublet: repairCosts.SubletTotalCost.toFormat(DineroFormat), + Towing: repairCosts.TowingTotalCost.toFormat(DineroFormat), + ATS: Dinero().toFormat(DineroFormat), + Storage: repairCosts.StorageTotalCost.toFormat(DineroFormat), + CostTotal: repairCosts.TotalCost.toFormat(DineroFormat), + }, + CostHours: { + Aluminum: repairCosts.AluminumLabourTotalHrs.toFixed(2), + Body: repairCosts.BodyLabourTotalHrs.toFixed(2), + Diagnostic: repairCosts.DiagnosticLabourTotalHrs.toFixed(2), + Refinish: repairCosts.RefinishLabourTotalHrs.toFixed(2), + Frame: repairCosts.FrameLabourTotalHrs.toFixed(2), + Mechanical: repairCosts.MechanicalLabourTotalHrs.toFixed(2), + Glass: repairCosts.GlassLabourTotalHrs.toFixed(2), + Electrical: repairCosts.ElectricalLabourTotalHrs.toFixed(2), + Structural: repairCosts.StructuralLabourTotalHrs.toFixed(2), + Other: repairCosts.LabourMiscTotalHrs.toFixed(2), + CostTotalHours: repairCosts.TotalHrs.toFixed(2), + }, + }; + return ret; + } catch (error) { + logger.log("kaizen-job-calculate-error", "ERROR", "api", null, { + error, + }); + + errorCallback({ jobid: job.id, ro_number: job.ro_number, error }); + } +}; + +const CreateCosts = (job) => { + //Create a mapping based on AH Requirements + + //For DMS, the keys in the object below are the CIECA part types. + const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => { + //At the bill level. + bill_val.billlines.map((line_val) => { + //At the bill line level. + + if (!bill_acc[line_val.cost_center]) + bill_acc[line_val.cost_center] = Dinero(); + + bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + + return null; + }); + return bill_acc; + }, {}); + + //If the hourly rates for job costing are set, add them in. + if ( + job.bodyshop.jc_hourly_rates && + (job.bodyshop.jc_hourly_rates.mapa || + typeof job.bodyshop.jc_hourly_rates.mapa === "number" || + isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) + ) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero(); + if (job.bodyshop.use_paint_scale_data === true) { + if (job.mixdata.length > 0) { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero({ + amount: Math.round( + ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 + ), + }); + } else { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mapa.hours) + ); + } + } else { + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mapa.hours) + ); + } + } + if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { + if ( + !billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] + ) + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = Dinero(); + billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = billTotalsByCostCenters[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mash * 100) || + 0 + ), + }).multiply(job.job_totals.rates.mash.hours) + ); + } + //Uses CIECA Labour types. + const ticketTotalsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = Dinero(); + + ticket_acc[ticket_val.cost_center] = ticket_acc[ + ticket_val.cost_center + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0 + ) + ); + + return ticket_acc; + }, + {} + ); + const ticketHrsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = 0; + + ticket_acc[ticket_val.cost_center] = + ticket_acc[ticket_val.cost_center] + + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0; + + return ticket_acc; + }, + {} + ); + //CIECA STANDARD MAPPING OBJECT. + + const ciecaObj = { + ATS: "ATS", + LA1: "LA1", + LA2: "LA2", + LA3: "LA3", + LA4: "LA4", + LAA: "LAA", + LAB: "LAB", + LAD: "LAD", + LAE: "LAE", + LAF: "LAF", + LAG: "LAG", + LAM: "LAM", + LAR: "LAR", + LAS: "LAS", + LAU: "LAU", + PAA: "PAA", + PAC: "PAC", + PAG: "PAG", + PAL: "PAL", + PAM: "PAM", + PAN: "PAN", + PAO: "PAO", + PAP: "PAP", + PAR: "PAR", + PAS: "PAS", + TOW: "TOW", + MAPA: "MAPA", + MASH: "MASH", + PASL: "PASL", + }; + const defaultCosts = + job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber + ? ciecaObj + : job.bodyshop.md_responsibility_centers.defaults.costs; + + return { + PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + if ( + key !== defaultCosts.PAS && + key !== defaultCosts.PASL && + key !== defaultCosts.MAPA && + key !== defaultCosts.MASH && + key !== defaultCosts.TOW + ) + return acc.add(billTotalsByCostCenters[key]); + return acc; + }, Dinero()), + PartsOemCost: (billTotalsByCostCenters[defaultCosts.PAN] || Dinero()).add( + billTotalsByCostCenters[defaultCosts.PAP] || Dinero() + ), + PartsAMCost: billTotalsByCostCenters[defaultCosts.PAA] || Dinero(), + PartsReconditionedCost: + billTotalsByCostCenters[defaultCosts.PAM] || Dinero(), + PartsRecycledCost: billTotalsByCostCenters[defaultCosts.PAL] || Dinero(), + PartsOtherCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + + SubletTotalCost: + billTotalsByCostCenters[defaultCosts.PAS] || + Dinero(billTotalsByCostCenters[defaultCosts.PASL] || Dinero()), + + AluminumLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAA] || Dinero(), + AluminumLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAA] || 0, + BodyLabourTotalCost: ticketTotalsByCostCenter[defaultCosts.LAB] || Dinero(), + BodyLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAB] || 0, + DiagnosticLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAD] || Dinero(), + DiagnosticLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAD] || 0, + ElectricalLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAE] || Dinero(), + ElectricalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAE] || 0, + FrameLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAF] || Dinero(), + FrameLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAF] || 0, + GlassLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAG] || Dinero(), + GlassLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAG] || 0, + LabourMiscTotalCost: ( + ticketTotalsByCostCenter[defaultCosts.LA1] || Dinero() + ) + .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA2] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA3] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LA4] || Dinero()) + .add(ticketTotalsByCostCenter[defaultCosts.LAU] || Dinero()), + LabourMiscTotalHrs: + (ticketHrsByCostCenter[defaultCosts.LA1] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA2] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA3] || 0) + + (ticketHrsByCostCenter[defaultCosts.LA4] || 0) + + (ticketHrsByCostCenter[defaultCosts.LAU] || 0), + MechanicalLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAM] || Dinero(), + MechanicalLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAM] || 0, + RefinishLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAR] || Dinero(), + RefinishLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAR] || 0, + StructuralLabourTotalCost: + ticketTotalsByCostCenter[defaultCosts.LAS] || Dinero(), + StructuralLabourTotalHrs: ticketHrsByCostCenter[defaultCosts.LAS] || 0, + + PMTotalCost: billTotalsByCostCenters[defaultCosts.MAPA] || Dinero(), + BMTotalCost: billTotalsByCostCenters[defaultCosts.MASH] || Dinero(), + + MiscTotalCost: billTotalsByCostCenters[defaultCosts.PAO] || Dinero(), + TowingTotalCost: billTotalsByCostCenters[defaultCosts.TOW] || Dinero(), + StorageTotalCost: Dinero(), + DetailTotal: Dinero(), + DetailTotalCost: Dinero(), + + SalesTaxTotalCost: Dinero(), + LabourTotalCost: Object.keys(ticketTotalsByCostCenter).reduce( + (acc, key) => { + return acc.add(ticketTotalsByCostCenter[key]); + }, + Dinero() + ), + TotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => { + return acc.add(billTotalsByCostCenters[key]); + }, Dinero()), + TotalHrs: job.timetickets.reduce((acc, ticket_val) => { + return ( + acc + + (ticket_val.flat_rate + ? ticket_val.productivehrs + : ticket_val.actualhrs) || 0 + ); + }, 0), + }; +}; diff --git a/server/email/sendemail.js b/server/email/sendemail.js index 640d24f2e..170504ce2 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -1,269 +1,269 @@ const path = require("path"); require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); const axios = require("axios"); let nodemailer = require("nodemailer"); let aws = require("@aws-sdk/client-ses"); -let { defaultProvider } = require("@aws-sdk/credential-provider-node"); +let {defaultProvider} = require("@aws-sdk/credential-provider-node"); const logger = require("../utils/logger"); const client = require("../graphql-client/graphql-client").client; const queries = require("../graphql-client/queries"); const ses = new aws.SES({ - // The key apiVersion is no longer supported in v3, and can be removed. - // @deprecated The client uses the "latest" apiVersion. - apiVersion: "latest", - region: "ca-central-1", - defaultProvider + // The key apiVersion is no longer supported in v3, and can be removed. + // @deprecated The client uses the "latest" apiVersion. + apiVersion: "latest", + region: "ca-central-1", + defaultProvider }); let transporter = nodemailer.createTransport({ - SES: { ses, aws }, + SES: {ses, aws}, }); -exports.sendServerEmail = async function ({ subject, text }) { - if (process.env.NODE_ENV === undefined) return; - try { - transporter.sendMail( - { - from: `ImEX Online API - ${process.env.NODE_ENV} `, - to: ["patrick@imexsystems.ca", "support@thinkimex.com"], - subject: subject, - text: text, - ses: { - // optional extra arguments for SendRawEmail - Tags: [ +exports.sendServerEmail = async function ({subject, text}) { + if (process.env.NODE_ENV === undefined) return; + try { + transporter.sendMail( { - Name: "tag_name", - Value: "tag_value", + from: `ImEX Online API - ${process.env.NODE_ENV} `, + to: ["patrick@imexsystems.ca", "support@thinkimex.com"], + subject: subject, + text: text, + ses: { + // optional extra arguments for SendRawEmail + Tags: [ + { + Name: "tag_name", + Value: "tag_value", + }, + ], + }, }, - ], - }, - }, - (err, info) => { - console.log(err || info); - } - ); - } catch (error) { - console.log(error); - logger.log("server-email-failure", "error", null, null, error); - } + (err, info) => { + console.log(err || info); + } + ); + } catch (error) { + console.log(error); + logger.log("server-email-failure", "error", null, null, error); + } }; -exports.sendTaskEmail = async function ({ to, subject, text, attachments }) { - try { - transporter.sendMail( - { - from: `ImEX Online `, - to: to, - subject: subject, - text: text, - attachments: attachments || null, - }, - (err, info) => { - console.log(err || info); - } - ); - } catch (error) { - console.log(error); - logger.log("server-email-failure", "error", null, null, error); - } +exports.sendTaskEmail = async function ({to, subject, text, attachments}) { + try { + transporter.sendMail( + { + from: `ImEX Online `, + to: to, + subject: subject, + text: text, + attachments: attachments || null, + }, + (err, info) => { + console.log(err || info); + } + ); + } catch (error) { + console.log(error); + logger.log("server-email-failure", "error", null, null, error); + } }; exports.sendEmail = async (req, res) => { - logger.log("send-email", "DEBUG", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - }); + logger.log("send-email", "DEBUG", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + }); - let downloadedMedia = []; - if (req.body.media && req.body.media.length > 0) { - downloadedMedia = await Promise.all( - req.body.media.map((m) => { - try { - return getImage(m); - } catch (error) { - logger.log("send-email-error", "ERROR", req.user.email, null, { + let downloadedMedia = []; + if (req.body.media && req.body.media.length > 0) { + downloadedMedia = await Promise.all( + req.body.media.map((m) => { + try { + return getImage(m); + } catch (error) { + logger.log("send-email-error", "ERROR", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + error, + }); + } + }) + ); + } + + transporter.sendMail( + { from: `${req.body.from.name} <${req.body.from.address}>`, replyTo: req.body.ReplyTo.Email, to: req.body.to, cc: req.body.cc, subject: req.body.subject, - error, - }); + attachments: + [ + ...((req.body.attachments && + req.body.attachments.map((a) => { + return { + filename: a.filename, + path: a.path, + }; + })) || + []), + ...downloadedMedia.map((a) => { + return { + path: a, + }; + }), + ] || null, + html: req.body.html, + ses: { + // optional extra arguments for SendRawEmail + Tags: [ + { + Name: "tag_name", + Value: "tag_value", + }, + ], + }, + }, + (err, info) => { + console.log(err || info); + if (info) { + logger.log("send-email-success", "DEBUG", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + // info, + }); + logEmail(req, { + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + messageId: info.response, + }); + res.json({ + success: true, //response: info + }); + } else { + logger.log("send-email-failure", "ERROR", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + replyTo: req.body.ReplyTo.Email, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + error: err, + }); + logEmail(req, { + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + bodyshopid: req.body.bodyshopid, + }); + res.status(500).json({success: false, error: err}); + } } - }) ); - } - - transporter.sendMail( - { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - attachments: - [ - ...((req.body.attachments && - req.body.attachments.map((a) => { - return { - filename: a.filename, - path: a.path, - }; - })) || - []), - ...downloadedMedia.map((a) => { - return { - path: a, - }; - }), - ] || null, - html: req.body.html, - ses: { - // optional extra arguments for SendRawEmail - Tags: [ - { - Name: "tag_name", - Value: "tag_value", - }, - ], - }, - }, - (err, info) => { - console.log(err || info); - if (info) { - logger.log("send-email-success", "DEBUG", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - // info, - }); - logEmail(req, { - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - messageId: info.response, - }); - res.json({ - success: true, //response: info - }); - } else { - logger.log("send-email-failure", "ERROR", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - replyTo: req.body.ReplyTo.Email, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - error: err, - }); - logEmail(req, { - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - bodyshopid: req.body.bodyshopid, - }); - res.status(500).json({ success: false, error: err }); - } - } - ); }; async function getImage(imageUrl) { - let image = await axios.get(imageUrl, { responseType: "arraybuffer" }); - let raw = Buffer.from(image.data).toString("base64"); - return "data:" + image.headers["content-type"] + ";base64," + raw; + let image = await axios.get(imageUrl, {responseType: "arraybuffer"}); + let raw = Buffer.from(image.data).toString("base64"); + return "data:" + image.headers["content-type"] + ";base64," + raw; } async function logEmail(req, email) { - try { - const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, { - email: { - to: email.to, - cc: email.cc, - subject: email.subject, - bodyshopid: req.body.bodyshopid, - useremail: req.user.email, - contents: req.body.html, - jobid: req.body.jobid, - sesmessageid: email.messageId, - status: "Sent", - }, - }); - console.log(insertresult); - } catch (error) { - logger.log("email-log-error", "error", req.user.email, null, { - from: `${req.body.from.name} <${req.body.from.address}>`, - to: req.body.to, - cc: req.body.cc, - subject: req.body.subject, - // info, - }); - } + try { + const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, { + email: { + to: email.to, + cc: email.cc, + subject: email.subject, + bodyshopid: req.body.bodyshopid, + useremail: req.user.email, + contents: req.body.html, + jobid: req.body.jobid, + sesmessageid: email.messageId, + status: "Sent", + }, + }); + console.log(insertresult); + } catch (error) { + logger.log("email-log-error", "error", req.user.email, null, { + from: `${req.body.from.name} <${req.body.from.address}>`, + to: req.body.to, + cc: req.body.cc, + subject: req.body.subject, + // info, + }); + } } -exports.emailBounce = async function (req, res, next) { - try { - const body = JSON.parse(req.body); - if (body.Type === "SubscriptionConfirmation") { - logger.log("SNS-message", "DEBUG", "api", null, { - body: req.body, - }); - } - const message = JSON.parse(body.Message); - if (message.notificationType === "Bounce") { - let replyTo, subject, messageId; - message.mail.headers.forEach((header) => { - if (header.name === "Reply-To") { - replyTo = header.value; - } else if (header.name === "Subject") { - subject = header.value; +exports.emailBounce = async function (req, res) { + try { + const body = JSON.parse(req.body); + if (body.Type === "SubscriptionConfirmation") { + logger.log("SNS-message", "DEBUG", "api", null, { + body: req.body, + }); } - }); - messageId = message.mail.messageId; - if (replyTo === "noreply@imex.online") { - res.sendStatus(200); - return; - } - //If it's bounced, log it as bounced in audit log. Send an email to the user. - const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { - sesid: messageId, - status: "Bounced", - context: message.bounce?.bouncedRecipients, - }); - transporter.sendMail( - { - from: `ImEX Online `, - to: replyTo, - //bcc: "patrick@snapt.ca", - subject: `ImEX Online Bounced Email - RE: ${subject}`, - text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. + const message = JSON.parse(body.Message); + if (message.notificationType === "Bounce") { + let replyTo, subject, messageId; + message.mail.headers.forEach((header) => { + if (header.name === "Reply-To") { + replyTo = header.value; + } else if (header.name === "Subject") { + subject = header.value; + } + }); + messageId = message.mail.messageId; + if (replyTo === "noreply@imex.online") { + res.sendStatus(200); + return; + } + //If it's bounced, log it as bounced in audit log. Send an email to the user. + const result = await client.request(queries.UPDATE_EMAIL_AUDIT, { + sesid: messageId, + status: "Bounced", + context: message.bounce?.bouncedRecipients, + }); + transporter.sendMail( + { + from: `ImEX Online `, + to: replyTo, + //bcc: "patrick@snapt.ca", + subject: `ImEX Online Bounced Email - RE: ${subject}`, + text: `ImEX Online has tried to deliver an email with the subject: ${subject} to the intended recipients but encountered an error. ${body.bounce?.bouncedRecipients.map( - (r) => - `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} + (r) => + `Recipient: ${r.emailAddress} | Status: ${r.action} | Code: ${r.diagnosticCode} ` -)} + )} `, - }, - (err, info) => { - console.log("***", err || info); + }, + (err, info) => { + console.log("***", err || info); + } + ); } - ); + } catch (error) { + logger.log("sns-error", "ERROR", "api", null, { + error: JSON.stringify(error), + }); } - } catch (error) { - logger.log("sns-error", "ERROR", "api", null, { - error: JSON.stringify(error), - }); - } - res.sendStatus(200); + res.sendStatus(200); }; diff --git a/server/firebase/firebase-handler.js b/server/firebase/firebase-handler.js index 7221649ec..d8cf63fb6 100644 --- a/server/firebase/firebase-handler.js +++ b/server/firebase/firebase-handler.js @@ -1,287 +1,215 @@ -var admin = require("firebase-admin"); +const admin = require("firebase-admin"); const logger = require("../utils/logger"); const path = require("path"); -const { auth } = require("firebase-admin"); +const {auth} = require("firebase-admin"); + require("dotenv").config({ - path: path.resolve( - process.cwd(), - `.env.${process.env.NODE_ENV || "development"}` - ), + path: path.resolve( + process.cwd(), + `.env.${process.env.NODE_ENV || "development"}` + ), }); const client = require("../graphql-client/graphql-client").client; -var serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); + +const serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON); +const adminEmail = require("../utils/adminEmail"); admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), - databaseURL: process.env.FIREBASE_DATABASE_URL, + credential: admin.credential.cert(serviceAccount), + databaseURL: process.env.FIREBASE_DATABASE_URL, }); exports.admin = admin; -const adminEmail = [ - "patrick@imex.dev", - //"patrick@imex.test", - "patrick@imex.prod", - "patrick@imexsystems.ca", - "patrick@thinkimex.com", -]; - exports.createUser = async (req, res) => { - logger.log("admin-create-user", "ADMIN", req.user.email, null, { - request: req.body, - ioadmin: true, - }); + logger.log("admin-create-user", "ADMIN", req.user.email, null, { + request: req.body, + ioadmin: true, + }); - const { email, displayName, password, shopid, authlevel } = req.body; - try { - const userRecord = await admin - .auth() - .createUser({ email, displayName, password }); + const {email, displayName, password, shopid, authlevel} = req.body; + try { + const userRecord = await admin + .auth() + .createUser({email, displayName, password}); - // See the UserRecord reference doc for the contents of userRecord. + // See the UserRecord reference doc for the contents of userRecord. - const result = await client.request( - ` + const result = await client.request( + ` mutation INSERT_USER($user: users_insert_input!) { insert_users_one(object: $user) { email } } `, - { - user: { - email, - authid: userRecord.uid, - associations: { - data: [{ shopid, authlevel, active: true }], - }, - }, - } - ); + { + user: { + email: email.toLowerCase(), + authid: userRecord.uid, + associations: { + data: [{shopid, authlevel, active: true}], + }, + }, + } + ); - res.json({ userRecord, result }); - } catch (error) { - logger.log("admin-update-user-error", "ERROR", req.user.email, null, { - error, - }); - res.status(500).json(error); - } + res.json({userRecord, result}); + } catch (error) { + logger.log("admin-update-user-error", "ERROR", req.user.email, null, { + error, + }); + res.status(500).json(error); + } }; exports.updateUser = (req, res) => { - logger.log("admin-update-user", "ADMIN", req.user.email, null, { - request: req.body, - ioadmin: true, - }); - - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log( - "admin-update-user-unauthorized", - "ERROR", - req.user.email, - null, - { + logger.log("admin-update-user", "ADMIN", req.user.email, null, { request: req.body, - user: req.user, - } - ); - res.sendStatus(404); - return; - } - - admin - .auth() - .updateUser( - req.body.uid, - req.body.user - // { - // email: "modifiedUser@example.com", - // phoneNumber: "+11234567890", - // emailVerified: true, - // password: "newPassword", - // displayName: "Jane Doe", - // photoURL: "http://www.example.com/12345678/photo.png", - // disabled: true, - // } - ) - .then((userRecord) => { - // See the UserRecord reference doc for the contents of userRecord. - - logger.log("admin-update-user-success", "ADMIN", req.user.email, null, { - userRecord, ioadmin: true, - }); - res.json(userRecord); - }) - .catch((error) => { - logger.log("admin-update-user-error", "ERROR", req.user.email, null, { - error, - }); - res.status(500).json(error); }); + + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log( + "admin-update-user-unauthorized", + "ERROR", + req.user.email, + null, + { + request: req.body, + user: req.user, + } + ); + res.sendStatus(404); + return; + } + + admin + .auth() + .updateUser( + req.body.uid, + req.body.user + // { + // email: "modifiedUser@example.com", + // phoneNumber: "+11234567890", + // emailVerified: true, + // password: "newPassword", + // displayName: "Jane Doe", + // photoURL: "http://www.example.com/12345678/photo.png", + // disabled: true, + // } + ) + .then((userRecord) => { + // See the UserRecord reference doc for the contents of userRecord. + + logger.log("admin-update-user-success", "ADMIN", req.user.email, null, { + userRecord, + ioadmin: true, + }); + res.json(userRecord); + }) + .catch((error) => { + logger.log("admin-update-user-error", "ERROR", req.user.email, null, { + error, + }); + res.status(500).json(error); + }); }; exports.getUser = (req, res) => { - logger.log("admin-get-user", "ADMIN", req.user.email, null, { - request: req.body, - ioadmin: true, - }); - - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log( - "admin-update-user-unauthorized", - "ERROR", - req.user.email, - null, - { + logger.log("admin-get-user", "ADMIN", req.user.email, null, { request: req.body, - user: req.user, - } - ); - res.sendStatus(404); - return; - } - - admin - .auth() - .getUser(req.body.uid) - .then((userRecord) => { - res.json(userRecord); - }) - .catch((error) => { - logger.log("admin-get-user-error", "ERROR", req.user.email, null, { - error, - }); - res.status(500).json(error); + ioadmin: true, }); + + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log( + "admin-update-user-unauthorized", + "ERROR", + req.user.email, + null, + { + request: req.body, + user: req.user, + } + ); + res.sendStatus(404); + return; + } + + admin + .auth() + .getUser(req.body.uid) + .then((userRecord) => { + res.json(userRecord); + }) + .catch((error) => { + logger.log("admin-get-user-error", "ERROR", req.user.email, null, { + error, + }); + res.status(500).json(error); + }); }; exports.sendNotification = async (req, res) => { - setTimeout(() => { - // Send a message to the device corresponding to the provided - // registration token. - admin - .messaging() - .send({ - topic: "PRD_PATRICK-messaging", - notification: { - title: `ImEX Online Message - +16049992002`, - body: "Test Noti.", - //imageUrl: "https://thinkimex.com/img/io-fcm.png", - }, - data: { - type: "messaging-inbound", - conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297", - text: "Hello. ", - image_path: "", - phone_num: "+16049992002", - }, - }) - .then((response) => { - // Response is a message ID string. - console.log("Successfully sent message:", response); - }) - .catch((error) => { - console.log("Error sending message:", error); - }); + setTimeout(() => { + // Send a message to the device corresponding to the provided + // registration token. + admin + .messaging() + .send({ + topic: "PRD_PATRICK-messaging", + notification: { + title: `ImEX Online Message - +16049992002`, + body: "Test Noti.", + //imageUrl: "https://thinkimex.com/img/io-fcm.png", + }, + data: { + type: "messaging-inbound", + conversationid: "e0eb17c3-3a78-4e3f-b932-55ef35aa2297", + text: "Hello. ", + image_path: "", + phone_num: "+16049992002", + }, + }) + .then((response) => { + // Response is a message ID string. + console.log("Successfully sent message:", response); + }) + .catch((error) => { + console.log("Error sending message:", error); + }); - res.sendStatus(200); - }, 500); + res.sendStatus(200); + }, 500); }; exports.subscribe = async (req, res) => { - const result = await admin - .messaging() - .subscribeToTopic( - req.body.fcm_tokens, - `${req.body.imexshopid}-${req.body.type}` - ); + const result = await admin + .messaging() + .subscribeToTopic( + req.body.fcm_tokens, + `${req.body.imexshopid}-${req.body.type}` + ); - res.json(result); + res.json(result); }; exports.unsubscribe = async (req, res) => { - try { - const result = await admin - .messaging() - .unsubscribeFromTopic( - req.body.fcm_tokens, - `${req.body.imexshopid}-${req.body.type}` - ); + try { + const result = await admin + .messaging() + .unsubscribeFromTopic( + req.body.fcm_tokens, + `${req.body.imexshopid}-${req.body.type}` + ); - res.json(result); - } catch (error) { - res.sendStatus(500); - } + res.json(result); + } catch (error) { + res.sendStatus(500); + } }; -exports.validateFirebaseIdToken = async (req, res, next) => { - if ( - (!req.headers.authorization || - !req.headers.authorization.startsWith("Bearer ")) && - !(req.cookies && req.cookies.__session) - ) { - console.error("Unauthorized attempt. No authorization provided."); - res.status(403).send("Unauthorized"); - return; - } - - let idToken; - if ( - req.headers.authorization && - req.headers.authorization.startsWith("Bearer ") - ) { - // console.log('Found "Authorization" header'); - // Read the ID Token from the Authorization header. - idToken = req.headers.authorization.split("Bearer ")[1]; - } else if (req.cookies) { - //console.log('Found "__session" cookie'); - // Read the ID Token from cookie. - idToken = req.cookies.__session; - } else { - // No cookie - console.error("Unauthorized attempt. No cookie provided."); - logger.log("api-unauthorized-call", "WARN", null, null, { - req, - type: "no-cookie", - }); - res.status(403).send("Unauthorized"); - return; - } - - try { - const decodedIdToken = await admin.auth().verifyIdToken(idToken); - //console.log("ID Token correctly decoded", decodedIdToken); - req.user = decodedIdToken; - next(); - return; - } catch (error) { - logger.log("api-unauthorized-call", "WARN", null, null, { - path: req.path, - body: req.body, - - type: "unauthroized", - ...error, - }); - - res.status(401).send("Unauthorized"); - return; - } -}; - -exports.validateAdmin = async (req, res, next) => { - if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { - logger.log("admin-validation-failed", "ERROR", req.user.email, null, { - request: req.body, - user: req.user, - }); - res.sendStatus(404); - return; - } else { - next(); - return; - } -}; //Admin claims code. // const uid = "JEqqYlsadwPEXIiyRBR55fflfko1"; diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index a27ac8bd0..b2fd4e23e 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -518,6 +518,21 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = ` } }`; +exports.QUERY_TRANSITIONS_BY_JOBID = `query QUERY_TRANSITIONS_BY_JOBID($jobids: [uuid!]!) { + transitions(where: {jobid: {_in: $jobids}}, order_by: {end: desc}) { + start + end + value + prev_value + next_value + duration + type + created_at + updated_at + jobid + } +}`; + exports.QUERY_UPCOMING_APPOINTMENTS = `query QUERY_UPCOMING_APPOINTMENTS($now: timestamptz!, $jobId: uuid!) { jobs_by_pk(id: $jobId) { bodyshop { @@ -1070,6 +1085,183 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) { } }`; +exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) { + bodyshops_by_pk(id: $bodyshopid){ + id + shopname + address1 + city + state + zip_post + country + phone + last_name_first + md_ro_statuses + md_order_statuses + md_responsibility_centers + jc_hourly_rates + cdk_dealerid + pbs_serialnumber + use_paint_scale_data + timezone + } + jobs(where: {_and: [{updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) { + actual_completion + actual_delivery + actual_in + asgn_date + bills { + billlines { + actual_cost + cost_center + id + quantity + } + federal_tax_rate + id + is_credit_memo + local_tax_rate + state_tax_rate + } + created_at + clm_no + date_estimated + date_exported + date_invoiced + date_open + date_repairstarted + employee_body_rel { + first_name + last_name + employee_number + id + } + employee_csr_rel { + first_name + last_name + employee_number + id + } + employee_prep_rel { + first_name + last_name + employee_number + id + } + employee_refinish_rel { + first_name + last_name + employee_number + id + } + est_ct_fn + est_ct_ln + id + ins_co_nm + joblines(where: {removed: {_eq: false}}) { + act_price + billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) { + actual_cost + actual_price + quantity + bill { + vendor { + name + } + invoice_number + date + } + } + db_price + id + lbr_op + line_desc + line_ind + line_no + mod_lb_hrs + mod_lbr_ty + parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){ + parts_order{ + id + order_date + } + } + part_qty + part_type + profitcenter_part + profitcenter_labor + prt_dsmk_m + prt_dsmk_p + oem_partno + status + } + job_totals + loss_date + mixdata(limit: 1, order_by: {updated_at: desc}) { + jobid + totalliquidcost + } + ownr_addr1 + ownr_addr2 + ownr_city + ownr_co_nm + ownr_fn + ownr_ln + ownr_st + ownr_zip + parts_orders(limit: 1, order_by: {created_at: desc}) { + created_at + } + parts_tax_rates + plate_no + rate_la1 + rate_la2 + rate_la3 + rate_la4 + rate_laa + rate_lab + rate_lad + rate_lae + rate_laf + rate_lag + rate_lam + rate_lar + rate_las + rate_lau + rate_ma2s + rate_ma2t + rate_ma3s + rate_mabl + rate_macs + rate_mahw + rate_matd + rate_mapa + rate_mash + ro_number + scheduled_completion + scheduled_delivery + scheduled_in + status + timetickets { + id + rate + cost_center + actualhrs + productivehrs + flat_rate + } + tlos_ind + v_color + v_model_yr + v_model_desc + v_make_desc + v_vin + vehicle { + v_bstyle + } + } +}`; + exports.UPDATE_JOB = ` mutation UPDATE_JOB($jobId: uuid!, $job: jobs_set_input!) { update_jobs(where: { id: { _eq: $jobId } }, _set: $job) { @@ -1542,7 +1734,7 @@ exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS { } }`; -exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { +exports.GET_ENTEGRAL_SHOPS = `query GET_ENTEGRAL_SHOPS { bodyshops(where: {entegral_id: {_is_null: false}, _or: {entegral_id: {_neq: ""}}}){ id shopname @@ -1562,6 +1754,26 @@ exports.GET_ENTEGRAL_SHOPS = `query GET_AUTOHOUSE_SHOPS { } }`; +exports.GET_KAIZEN_SHOPS = `query GET_KAIZEN_SHOPS($imexshopid: [String]) { + bodyshops(where: {imexshopid: {_in: $imexshopid}}){ + id + shopname + address1 + city + state + zip_post + country + phone + md_ro_statuses + md_order_statuses + autohouseid + md_responsibility_centers + jc_hourly_rates + imexshopid + timezone + } +}`; + exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{ delete_dms_vehicles(where: {}) { affected_rows @@ -1777,12 +1989,20 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $ } }`; -exports.INSERT_NEW_TRANSITION = `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, $oldTransitionId: uuid, $duration: numeric) { +exports.INSERT_NEW_TRANSITION = ( + includeOldTransition +) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${ + includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : "" +}) { insert_transitions_one(object: $newTransition) { id } - update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) { + ${ + includeOldTransition + ? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) { affected_rows + }` + : "" } }`; @@ -1936,3 +2156,23 @@ exports.ACTIVE_SHOP_BY_USER = `query ACTIVE_SHOP_BY_USER($user: String) { shopid } }`; + +exports.QUERY_SURVEY = `query QUERY_SURVEY($surveyId: uuid!) { + csi_by_pk(id: $surveyId) { + completedon + csiquestion { + id + config + } + id + relateddata + valid + validuntil + } +}`; + +exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: csi_set_input) { + update_csi(where: { id: { _eq: $surveyId } }, _set: $survey) { + affected_rows + } + }`; \ No newline at end of file diff --git a/server/intellipay/intellipay.js b/server/intellipay/intellipay.js index 0ece0c69c..60b74c1b5 100644 --- a/server/intellipay/intellipay.js +++ b/server/intellipay/intellipay.js @@ -132,6 +132,7 @@ exports.payment_refund = async (req, res) => { exports.generate_payment_url = async (req, res) => { logger.log("intellipay-payment-url", "DEBUG", req.user?.email, null, null); const shopCredentials = await getShopCredentials(req.body.bodyshop); + try { const options = { method: "POST", @@ -139,7 +140,12 @@ exports.generate_payment_url = async (req, res) => { //TODO: Move these to environment variables/database. data: qs.stringify({ ...shopCredentials, - ...req.body, + //...req.body, + amount: Dinero({ amount: Math.round(req.body.amount * 100) }).toFormat( + "0.00" + ), + account: req.body.account, + invoice: req.body.invoice, createshorturl: true, //The postback URL is set at the CP teller global terminal settings page. }), diff --git a/server/job/job-costing.js b/server/job/job-costing.js index e7568fa72..4e7b40dc7 100644 --- a/server/job/job-costing.js +++ b/server/job/job-costing.js @@ -1,83 +1,752 @@ +const _ = require("lodash"); const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); -//const client = require("../graphql-client/graphql-client").client; -const _ = require("lodash"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); -const { DiscountNotAlreadyCounted } = require("./job-totals"); +const logger = require('../utils/logger'); +const {DiscountNotAlreadyCounted} = require("./job-totals"); + // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; async function JobCosting(req, res) { - const { jobid } = req.body; + const {jobid} = req.body; - const BearerToken = req.headers.authorization; - logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; - try { - const resp = await client - .setHeaders({ Authorization: BearerToken }) - .request(queries.QUERY_JOB_COSTING_DETAILS, { - id: jobid, - }); + logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null); - const ret = GenerateCostingData(resp.jobs_by_pk); + try { + const resp = await client + .setHeaders({Authorization: BearerToken}) + .request(queries.QUERY_JOB_COSTING_DETAILS, { + id: jobid, + }); - res.status(200).json(ret); - } catch (error) { - logger.log("job-costing-error", "ERROR", req.user.email, jobid, { - message: error.message, - stack: error.stack, - }); + const ret = GenerateCostingData(resp.jobs_by_pk); - res.status(400).send(JSON.stringify(error)); - } + res.status(200).json(ret); + } catch (error) { + logger.log("job-costing-error", "ERROR", req.user.email, jobid, { + message: error.message, + stack: error.stack, + }); + + res.status(400).send(JSON.stringify(error)); + } } async function JobCostingMulti(req, res) { - const { jobids } = req.body; - const BearerToken = req.headers.authorization; - logger.log("job-costing-multi-start", "DEBUG", req.user.email, jobids, null); + const {jobids} = req.body; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const logger = req.logger; + const BearerToken = req.BearerToken + const client = req.userGraphQLClient; - try { - const resp = await client - .setHeaders({ Authorization: BearerToken }) - .request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, { - ids: jobids, - }); + logger.log("job-costing-multi-start", "DEBUG", req.user.email, jobids, null); - const multiSummary = { - costCenterData: [], - summaryData: { - totalLaborSales: Dinero({ amount: 0 }), - totalPartsSales: Dinero({ amount: 0 }), - totalAdditionalSales: Dinero({ amount: 0 }), - totalSubletSales: Dinero({ amount: 0 }), - totalSales: Dinero({ amount: 0 }), - totalLaborCost: Dinero({ amount: 0 }), - totalPartsCost: Dinero({ amount: 0 }), - totalAdditionalCost: Dinero({ amount: 0 }), - totalSubletCost: Dinero({ amount: 0 }), - totalCost: Dinero({ amount: 0 }), - gpdollars: Dinero({ amount: 0 }), - gppercent: null, - gppercentFormatted: null, - totalLaborGp: Dinero({ amount: 0 }), - totalPartsGp: Dinero({ amount: 0 }), - totalAdditionalGp: Dinero({ amount: 0 }), - totalSubletGp: Dinero({ amount: 0 }), + + try { + const resp = await client + .setHeaders({Authorization: BearerToken}) + .request(queries.QUERY_JOB_COSTING_DETAILS_MULTI, { + ids: jobids, + }); + + const multiSummary = { + costCenterData: [], + summaryData: { + totalLaborSales: Dinero({amount: 0}), + totalPartsSales: Dinero({amount: 0}), + totalAdditionalSales: Dinero({amount: 0}), + totalSubletSales: Dinero({amount: 0}), + totalSales: Dinero({amount: 0}), + totalLaborCost: Dinero({amount: 0}), + totalPartsCost: Dinero({amount: 0}), + totalAdditionalCost: Dinero({amount: 0}), + totalSubletCost: Dinero({amount: 0}), + totalCost: Dinero({amount: 0}), + gpdollars: Dinero({amount: 0}), + gppercent: null, + gppercentFormatted: null, + totalLaborGp: Dinero({amount: 0}), + totalPartsGp: Dinero({amount: 0}), + totalAdditionalGp: Dinero({amount: 0}), + totalSubletGp: Dinero({amount: 0}), + totalLaborGppercent: null, + totalLaborGppercentFormatted: null, + totalPartsGppercent: null, + totalPartsGppercentFormatted: null, + totalAdditionalGppercent: null, + totalAdditionalGppercentFormatted: null, + totalSubletGppercent: null, + totalSubletGppercentFormatted: null, + }, + }; + + const ret = {}; + resp.jobs.map((job) => { + const costingData = GenerateCostingData(job); + ret[job.id] = costingData; + + //Merge on a cost center basis. + + costingData.costCenterData.forEach((c) => { + //Find the Cost Center if it exists. + + const CostCenterIndex = multiSummary.costCenterData.findIndex( + (x) => x.cost_center === c.cost_center + ); + + if (CostCenterIndex >= 0) { + //Add it in place + multiSummary.costCenterData[CostCenterIndex] = { + ...multiSummary.costCenterData[CostCenterIndex], + sale_labor_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_labor_dinero.add(c.sale_labor_dinero), + sale_parts_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_parts_dinero.add(c.sale_parts_dinero), + sale_additional_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_additional_dinero.add(c.sale_additional_dinero), + sale_sublet_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sale_sublet_dinero.add(c.sale_sublet_dinero), + cost_labor_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_labor_dinero.add(c.cost_labor_dinero), + cost_parts_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_parts_dinero.add(c.cost_parts_dinero), + cost_additional_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_additional_dinero.add(c.cost_additional_dinero), + cost_sublet_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].cost_sublet_dinero.add(c.cost_sublet_dinero), + gpdollars_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].gpdollars_dinero.add(c.gpdollars_dinero), + costs_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].costs_dinero.add(c.costs_dinero), + sales_dinero: multiSummary.costCenterData[ + CostCenterIndex + ].sales_dinero.add(c.sales_dinero), + }; + } else { + //Add it to the list instead. + multiSummary.costCenterData.push(c); + } + }); + + //Add all summary data. + multiSummary.summaryData.totalPartsSales = + multiSummary.summaryData.totalPartsSales.add( + costingData.summaryData.totalPartsSales + ); + multiSummary.summaryData.totalAdditionalSales = + multiSummary.summaryData.totalAdditionalSales.add( + costingData.summaryData.totalAdditionalSales + ); + multiSummary.summaryData.totalSubletSales = + multiSummary.summaryData.totalSubletSales.add( + costingData.summaryData.totalSubletSales + ); + multiSummary.summaryData.totalSales = + multiSummary.summaryData.totalSales.add( + costingData.summaryData.totalSales + ); + multiSummary.summaryData.totalLaborCost = + multiSummary.summaryData.totalLaborCost.add( + costingData.summaryData.totalLaborCost + ); + multiSummary.summaryData.totalLaborSales = + multiSummary.summaryData.totalLaborSales.add( + costingData.summaryData.totalLaborSales + ); + multiSummary.summaryData.totalPartsCost = + multiSummary.summaryData.totalPartsCost.add( + costingData.summaryData.totalPartsCost + ); + multiSummary.summaryData.totalAdditionalCost = + multiSummary.summaryData.totalAdditionalCost.add( + costingData.summaryData.totalAdditionalCost + ); + multiSummary.summaryData.totalSubletCost = + multiSummary.summaryData.totalSubletCost.add( + costingData.summaryData.totalSubletCost + ); + multiSummary.summaryData.totalCost = + multiSummary.summaryData.totalCost.add( + costingData.summaryData.totalCost + ); + multiSummary.summaryData.gpdollars = + multiSummary.summaryData.gpdollars.add( + costingData.summaryData.gpdollars + ); + + multiSummary.summaryData.totalLaborGp = + multiSummary.summaryData.totalLaborGp.add( + costingData.summaryData.totalLaborGp + ); + multiSummary.summaryData.totalPartsGp = + multiSummary.summaryData.totalPartsGp.add( + costingData.summaryData.totalPartsGp + ); + multiSummary.summaryData.totalAdditionalGp = + multiSummary.summaryData.totalAdditionalGp.add( + costingData.summaryData.totalAdditionalGp + ); + multiSummary.summaryData.totalSubletGp = + multiSummary.summaryData.totalSubletGp.add( + costingData.summaryData.totalSubletGp + ); + + //Take the summary data & add it to total summary data. + }); + + //For each center, recalculate and toFormat() the values. + + multiSummary.summaryData.totalLaborGppercent = ( + (multiSummary.summaryData.totalLaborGp.getAmount() / + multiSummary.summaryData.totalLaborSales.getAmount()) * + 100 + ).toFixed(1); + multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalLaborGppercent + ); + + multiSummary.summaryData.totalPartsGppercent = ( + (multiSummary.summaryData.totalPartsGp.getAmount() / + multiSummary.summaryData.totalPartsSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalPartsGppercent + ); + + multiSummary.summaryData.totalAdditionalGppercent = ( + (multiSummary.summaryData.totalAdditionalGp.getAmount() / + multiSummary.summaryData.totalAdditionalSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.totalAdditionalGppercentFormatted = + formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent); + + multiSummary.summaryData.totalSubletGppercent = ( + (multiSummary.summaryData.totalSubletGp.getAmount() / + multiSummary.summaryData.totalSubletSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent( + multiSummary.summaryData.totalSubletGppercent + ); + + multiSummary.summaryData.gppercent = ( + (multiSummary.summaryData.gpdollars.getAmount() / + multiSummary.summaryData.totalSales.getAmount()) * + 100 + ).toFixed(1); + + multiSummary.summaryData.gppercentFormatted = formatGpPercent( + multiSummary.summaryData.gppercent + ); + + const finalCostingdata = multiSummary.costCenterData.map((c) => { + return { + ...c, + sale_labor: c.sale_labor_dinero && c.sale_labor_dinero.toFormat(), + sale_parts: c.sale_parts_dinero && c.sale_parts_dinero.toFormat(), + sale_additional: + c.sale_additional_dinero && c.sale_additional_dinero.toFormat(), + sale_sublet: c.sale_sublet_dinero && c.sale_sublet_dinero.toFormat(), + sales: c.sales_dinero.toFormat(), + cost_parts: c.cost_parts_dinero && c.cost_parts_dinero.toFormat(), + cost_labor: c.cost_labor_dinero && c.cost_labor_dinero.toFormat(), + cost_additional: + c.cost_additional_dinero && c.cost_additional_dinero.toFormat(), + cost_sublet: c.cost_sublet_dinero && c.cost_sublet_dinero.toFormat(), + costs: c.costs_dinero.toFormat(), + gpdollars: c.gpdollars_dinero.toFormat(), + gppercent: formatGpPercent( + ( + (c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) * + 100 + ).toFixed(1) + ), + }; + }); + + //Calculate thte total gross profit percentages. + + res.status(200).json({ + allCostCenterData: finalCostingdata, + allSummaryData: multiSummary.summaryData, + data: ret, + }); + } catch (error) { + logger.log("job-costing-multi-error", "ERROR", req.user.email, [jobids], { + message: error.message, + stack: error.stack, + }); + res.status(400).send(error); + } +} + +function GenerateCostingData(job) { + const defaultProfits = + job.bodyshop.md_responsibility_centers.defaults.profits; + const allCenters = _.union( + job.bodyshop.md_responsibility_centers.profits.map((p) => p.name), + job.bodyshop.md_responsibility_centers.costs.map((p) => p.name), + ["Unknown"] + ); + + const materialsHours = {mapaHrs: 0, mashHrs: 0}; + let hasMapaLine = false; + let hasMashLine = false; + + //Massage the data. + const jobLineTotalsByProfitCenter = + job && + job.joblines.reduce( + (acc, val) => { + //Parts Lines + if (val.db_ref === "936008") { + //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. + hasMapaLine = true; + } + if (val.db_ref === "936007") { + hasMashLine = true; + } + if (val.mod_lbr_ty) { + const laborProfitCenter = + val.profitcenter_labor || + defaultProfits[val.mod_lbr_ty] || + "Unknown"; + + if (laborProfitCenter === "Unknown") + console.log("Unknown type", val.line_desc, val.mod_lbr_ty); + + const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`; + + const laborAmount = Dinero({ + amount: Math.round((job[rateName] || 0) * 100), + }).multiply(val.mod_lb_hrs || 0); + if (!acc.labor[laborProfitCenter]) + acc.labor[laborProfitCenter] = Dinero(); + acc.labor[laborProfitCenter] = + acc.labor[laborProfitCenter].add(laborAmount); + + if ( + val.mod_lb_hrs === 0 && + val.act_price > 0 && + val.lbr_op === "OP14" + ) { + //Scenario where SGI may pay out hours using a part price. + acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add( + Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }).multiply(val.part_qty) + ); + } + + if (val.mod_lbr_ty === "LAR") { + materialsHours.mapaHrs += val.mod_lb_hrs || 0; + } + if (val.mod_lbr_ty !== "LAR") { + materialsHours.mashHrs += val.mod_lb_hrs || 0; + } + } + + if ( + val.part_type && + val.part_type !== "PAE" && + val.part_type !== "PAS" && + val.part_type !== "PASL" + ) { + const partsProfitCenter = + val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; + + if (partsProfitCenter === "Unknown") + console.log("Unknown type", val.line_desc, val.part_type); + + if (!partsProfitCenter) + console.log( + "Unknown cost/profit center mapping for parts.", + val.line_desc, + val.part_type + ); + const partsAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 1) + .add( + ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || + (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({amount: Math.round(val.prt_dsmk_m * 100)}) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ); + if (!acc.parts[partsProfitCenter]) + acc.parts[partsProfitCenter] = Dinero(); + acc.parts[partsProfitCenter] = + acc.parts[partsProfitCenter].add(partsAmount); + } + if ( + val.part_type && + val.part_type !== "PAE" && + (val.part_type === "PAS" || val.part_type === "PASL") + ) { + const partsProfitCenter = + val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; + + if (partsProfitCenter === "Unknown") + console.log("Unknown type", val.line_desc, val.part_type); + + if (!partsProfitCenter) + console.log( + "Unknown cost/profit center mapping for sublet.", + val.line_desc, + val.part_type + ); + const partsAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 1) + .add( + ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || + (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({amount: Math.round(val.prt_dsmk_m * 100)}) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ); + if (!acc.sublet[partsProfitCenter]) + acc.sublet[partsProfitCenter] = Dinero(); + acc.sublet[partsProfitCenter] = + acc.sublet[partsProfitCenter].add(partsAmount); + } + + //To deal with additional costs. + if (!val.part_type && !val.mod_lbr_ty) { + //Does it already have a defined profit center? + //If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate. + const partsProfitCenter = + val.profitcenter_part || + getAdditionalCostCenter(val, defaultProfits) || + "Unknown"; + + if (partsProfitCenter === "Unknown") { + console.log("Unknown type", val.line_desc, val.part_type); + } + const partsAmount = Dinero({ + amount: Math.round((val.act_price || 0) * 100), + }) + .multiply(val.part_qty || 1) + .add( + ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || + (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && + DiscountNotAlreadyCounted(val, job.joblines) + ? val.prt_dsmk_m + ? Dinero({amount: Math.round(val.prt_dsmk_m * 100)}) + : Dinero({ + amount: Math.round(val.act_price * 100), + }) + .multiply(val.part_qty || 0) + .percentage(Math.abs(val.prt_dsmk_p || 0)) + .multiply(val.prt_dsmk_p > 0 ? 1 : -1) + : Dinero() + ); + + if (!acc.additional[partsProfitCenter]) + acc.additional[partsProfitCenter] = Dinero(); + acc.additional[partsProfitCenter] = + acc.additional[partsProfitCenter].add(partsAmount); + } + + return acc; + }, + {parts: {}, labor: {}, additional: {}, sublet: {}} + ); + + if (!hasMapaLine) { + if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]]) + jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero(); + jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = + jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]].add( + Dinero({ + amount: Math.round((job.rate_mapa || 0) * 100), + }).multiply(materialsHours.mapaHrs || 0) + ); + } + if (!hasMashLine) { + if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]]) + jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero(); + jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = + jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]].add( + Dinero({ + amount: Math.round((job.rate_mash || 0) * 100), + }).multiply(materialsHours.mashHrs || 0) + ); + } + + //Is it a DMS Setup? + const selectedDmsAllocationConfig = + (job.bodyshop.md_responsibility_centers.dms_defaults && + job.bodyshop.md_responsibility_centers.dms_defaults.find( + (d) => d.name === job.dms_allocation + )) || + job.bodyshop.md_responsibility_centers.defaults; + + const billTotalsByCostCenters = job.bills.reduce( + (bill_acc, bill_val) => { + //At the bill level. + bill_val.billlines.map((line_val) => { + //At the bill line level. + if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { + if ( + !bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] + ) + bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = + Dinero(); + + bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = + bill_acc[ + selectedDmsAllocationConfig.costs[line_val.cost_center] + ].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } else { + const isSubletCostCenter = + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.PAS || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.PASL; + + const isAdditionalCostCenter = + // line_val.cost_center === + // job.bodyshop.md_responsibility_centers.defaults.costs.PAS || + // line_val.cost_center === + // job.bodyshop.md_responsibility_centers.defaults.costs.PASL || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.TOW || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA || + line_val.cost_center === + job.bodyshop.md_responsibility_centers.defaults.costs.MASH; + + if (isAdditionalCostCenter) { + if (!bill_acc.additionalCosts[line_val.cost_center]) + bill_acc.additionalCosts[line_val.cost_center] = Dinero(); + + bill_acc.additionalCosts[line_val.cost_center] = + bill_acc.additionalCosts[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } else if (isSubletCostCenter) { + if (!bill_acc.subletCosts[line_val.cost_center]) + bill_acc.subletCosts[line_val.cost_center] = Dinero(); + + bill_acc.subletCosts[line_val.cost_center] = bill_acc.subletCosts[ + line_val.cost_center + ].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } else { + if (!bill_acc[line_val.cost_center]) + bill_acc[line_val.cost_center] = Dinero(); + + bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( + Dinero({ + amount: Math.round((line_val.actual_cost || 0) * 100), + }) + .multiply(line_val.quantity) + .multiply(bill_val.is_credit_memo ? -1 : 1) + ); + } + } + + return null; + }); + return bill_acc; + }, + {additionalCosts: {}, subletCosts: {}} + ); + + //If the hourly rates for job costing are set, add them in. + + if ( + job.bodyshop.jc_hourly_rates && + (job.bodyshop.jc_hourly_rates.mapa || + typeof job.bodyshop.jc_hourly_rates.mapa === "number" || + isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) + ) { + if ( + !billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] + ) + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero(); + if (job.bodyshop.use_paint_scale_data === true) { + if (job.mixdata.length > 0) { + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = Dinero({ + amount: Math.round( + ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 + ), + }); + } else { + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(materialsHours.mapaHrs) + ); + } + } else { + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ] = billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MAPA + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mapa * 100) || + 0 + ), + }).multiply(materialsHours.mapaHrs) + ); + } + } + + if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { + if ( + !billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] + ) + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = Dinero(); + billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ] = billTotalsByCostCenters.additionalCosts[ + job.bodyshop.md_responsibility_centers.defaults.costs.MASH + ].add( + Dinero({ + amount: Math.round( + (job.bodyshop.jc_hourly_rates && + job.bodyshop.jc_hourly_rates.mash * 100) || + 0 + ), + }).multiply(materialsHours.mashHrs) + ); + } + + const ticketTotalsByCostCenter = job.timetickets.reduce( + (ticket_acc, ticket_val) => { + //At the invoice level. + + if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { + if ( + !ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] + ) + ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = + Dinero(); + + ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = + ticket_acc[ + selectedDmsAllocationConfig.costs[ticket_val.ciecacode] + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + ticket_val.flat_rate + ? ticket_val.productivehrs || ticket_val.actualhrs || 0 + : ticket_val.actualhrs || ticket_val.productivehrs || 0 + ) //Should base this on the employee. + ); + } else { + if (!ticket_acc[ticket_val.cost_center]) + ticket_acc[ticket_val.cost_center] = Dinero(); + + ticket_acc[ticket_val.cost_center] = ticket_acc[ + ticket_val.cost_center + ].add( + Dinero({ + amount: Math.round((ticket_val.rate || 0) * 100), + }).multiply( + ticket_val.flat_rate + ? ticket_val.productivehrs || ticket_val.actualhrs || 0 + : ticket_val.actualhrs || ticket_val.productivehrs || 0 + ) //Should base this on the employee. + ); + } + + return ticket_acc; + }, + {} + ); + + const summaryData = { + totalLaborSales: Dinero({amount: 0}), + totalPartsSales: Dinero({amount: 0}), + totalAdditionalSales: Dinero({amount: 0}), + totalSubletSales: Dinero({amount: 0}), + totalSales: Dinero({amount: 0}), + totalLaborCost: Dinero({amount: 0}), + totalPartsCost: Dinero({amount: 0}), + totalAdditionalCost: Dinero({amount: 0}), + totalSubletCost: Dinero({amount: 0}), + totalCost: Dinero({amount: 0}), + totalLaborGp: Dinero({amount: 0}), + totalPartsGp: Dinero({amount: 0}), + totalAdditionalGp: Dinero({amount: 0}), + totalSubletGp: Dinero({amount: 0}), + gpdollars: Dinero({amount: 0}), totalLaborGppercent: null, totalLaborGppercentFormatted: null, totalPartsGppercent: null, @@ -86,894 +755,220 @@ async function JobCostingMulti(req, res) { totalAdditionalGppercentFormatted: null, totalSubletGppercent: null, totalSubletGppercentFormatted: null, - }, + gppercent: null, + gppercentFormatted: null, }; - const ret = {}; - resp.jobs.map((job) => { - const costingData = GenerateCostingData(job); - ret[job.id] = costingData; + const costCenterData = allCenters.map((key, idx) => { + const ccVal = key; // defaultProfits[key]; + const sale_labor = + jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({amount: 0}); + const sale_parts = + jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({amount: 0}); + const sale_additional = + jobLineTotalsByProfitCenter.additional[ccVal] || Dinero({amount: 0}); + const sale_sublet = + jobLineTotalsByProfitCenter.sublet[ccVal] || Dinero({amount: 0}); - //Merge on a cost center basis. + const cost_labor = ticketTotalsByCostCenter[ccVal] || Dinero({amount: 0}); + const cost_parts = billTotalsByCostCenters[ccVal] || Dinero({amount: 0}); + const cost_additional = + billTotalsByCostCenters.additionalCosts[ccVal] || Dinero({amount: 0}); + const cost_sublet = + billTotalsByCostCenters.subletCosts[ccVal] || Dinero({amount: 0}); - costingData.costCenterData.forEach((c) => { - //Find the Cost Center if it exists. - - const CostCenterIndex = multiSummary.costCenterData.findIndex( - (x) => x.cost_center === c.cost_center - ); - - if (CostCenterIndex >= 0) { - //Add it in place - multiSummary.costCenterData[CostCenterIndex] = { - ...multiSummary.costCenterData[CostCenterIndex], - sale_labor_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_labor_dinero.add(c.sale_labor_dinero), - sale_parts_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_parts_dinero.add(c.sale_parts_dinero), - sale_additional_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_additional_dinero.add(c.sale_additional_dinero), - sale_sublet_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sale_sublet_dinero.add(c.sale_sublet_dinero), - cost_labor_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_labor_dinero.add(c.cost_labor_dinero), - cost_parts_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_parts_dinero.add(c.cost_parts_dinero), - cost_additional_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_additional_dinero.add(c.cost_additional_dinero), - cost_sublet_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].cost_sublet_dinero.add(c.cost_sublet_dinero), - gpdollars_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].gpdollars_dinero.add(c.gpdollars_dinero), - costs_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].costs_dinero.add(c.costs_dinero), - sales_dinero: multiSummary.costCenterData[ - CostCenterIndex - ].sales_dinero.add(c.sales_dinero), - }; - } else { - //Add it to the list instead. - multiSummary.costCenterData.push(c); - } - }); - - //Add all summary data. - multiSummary.summaryData.totalPartsSales = - multiSummary.summaryData.totalPartsSales.add( - costingData.summaryData.totalPartsSales - ); - multiSummary.summaryData.totalAdditionalSales = - multiSummary.summaryData.totalAdditionalSales.add( - costingData.summaryData.totalAdditionalSales - ); - multiSummary.summaryData.totalSubletSales = - multiSummary.summaryData.totalSubletSales.add( - costingData.summaryData.totalSubletSales - ); - multiSummary.summaryData.totalSales = - multiSummary.summaryData.totalSales.add( - costingData.summaryData.totalSales - ); - multiSummary.summaryData.totalLaborCost = - multiSummary.summaryData.totalLaborCost.add( - costingData.summaryData.totalLaborCost - ); - multiSummary.summaryData.totalLaborSales = - multiSummary.summaryData.totalLaborSales.add( - costingData.summaryData.totalLaborSales - ); - multiSummary.summaryData.totalPartsCost = - multiSummary.summaryData.totalPartsCost.add( - costingData.summaryData.totalPartsCost - ); - multiSummary.summaryData.totalAdditionalCost = - multiSummary.summaryData.totalAdditionalCost.add( - costingData.summaryData.totalAdditionalCost - ); - multiSummary.summaryData.totalSubletCost = - multiSummary.summaryData.totalSubletCost.add( - costingData.summaryData.totalSubletCost - ); - multiSummary.summaryData.totalCost = - multiSummary.summaryData.totalCost.add( - costingData.summaryData.totalCost - ); - multiSummary.summaryData.gpdollars = - multiSummary.summaryData.gpdollars.add( - costingData.summaryData.gpdollars - ); - - multiSummary.summaryData.totalLaborGp = - multiSummary.summaryData.totalLaborGp.add( - costingData.summaryData.totalLaborGp - ); - multiSummary.summaryData.totalPartsGp = - multiSummary.summaryData.totalPartsGp.add( - costingData.summaryData.totalPartsGp - ); - multiSummary.summaryData.totalAdditionalGp = - multiSummary.summaryData.totalAdditionalGp.add( - costingData.summaryData.totalAdditionalGp - ); - multiSummary.summaryData.totalSubletGp = - multiSummary.summaryData.totalSubletGp.add( - costingData.summaryData.totalSubletGp - ); - - //Take the summary data & add it to total summary data. - }); - - //For each center, recalculate and toFormat() the values. - - multiSummary.summaryData.totalLaborGppercent = ( - (multiSummary.summaryData.totalLaborGp.getAmount() / - multiSummary.summaryData.totalLaborSales.getAmount()) * - 100 - ).toFixed(1); - multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent( - multiSummary.summaryData.totalLaborGppercent - ); - - multiSummary.summaryData.totalPartsGppercent = ( - (multiSummary.summaryData.totalPartsGp.getAmount() / - multiSummary.summaryData.totalPartsSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent( - multiSummary.summaryData.totalPartsGppercent - ); - - multiSummary.summaryData.totalAdditionalGppercent = ( - (multiSummary.summaryData.totalAdditionalGp.getAmount() / - multiSummary.summaryData.totalAdditionalSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.totalAdditionalGppercentFormatted = - formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent); - - multiSummary.summaryData.totalSubletGppercent = ( - (multiSummary.summaryData.totalSubletGp.getAmount() / - multiSummary.summaryData.totalSubletSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent( - multiSummary.summaryData.totalSubletGppercent - ); - - multiSummary.summaryData.gppercent = ( - (multiSummary.summaryData.gpdollars.getAmount() / - multiSummary.summaryData.totalSales.getAmount()) * - 100 - ).toFixed(1); - - multiSummary.summaryData.gppercentFormatted = formatGpPercent( - multiSummary.summaryData.gppercent - ); - - const finalCostingdata = multiSummary.costCenterData.map((c) => { - return { - ...c, - sale_labor: c.sale_labor_dinero && c.sale_labor_dinero.toFormat(), - sale_parts: c.sale_parts_dinero && c.sale_parts_dinero.toFormat(), - sale_additional: - c.sale_additional_dinero && c.sale_additional_dinero.toFormat(), - sale_sublet: c.sale_sublet_dinero && c.sale_sublet_dinero.toFormat(), - sales: c.sales_dinero.toFormat(), - cost_parts: c.cost_parts_dinero && c.cost_parts_dinero.toFormat(), - cost_labor: c.cost_labor_dinero && c.cost_labor_dinero.toFormat(), - cost_additional: - c.cost_additional_dinero && c.cost_additional_dinero.toFormat(), - cost_sublet: c.cost_sublet_dinero && c.cost_sublet_dinero.toFormat(), - costs: c.costs_dinero.toFormat(), - gpdollars: c.gpdollars_dinero.toFormat(), - gppercent: formatGpPercent( - ( - (c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) * + const costs = cost_labor + .add(cost_parts) + .add(cost_additional) + .add(cost_sublet); + const totalSales = sale_labor + .add(sale_parts) + .add(sale_additional) + .add(sale_sublet); + const gpdollars = totalSales.subtract(costs); + const gppercent = ( + (gpdollars.getAmount() / Math.abs(totalSales.getAmount())) * 100 - ).toFixed(1) - ), - }; + ).toFixed(1); + + //Push summary data to avoid extra loop. + summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor); + summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts); + summaryData.totalAdditionalSales = + summaryData.totalAdditionalSales.add(sale_additional); + summaryData.totalSubletSales = + summaryData.totalSubletSales.add(sale_sublet); + summaryData.totalSales = summaryData.totalSales.add(totalSales); + summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor); + summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts); + summaryData.totalAdditionalCost = + summaryData.totalAdditionalCost.add(cost_additional); + summaryData.totalSubletCost = summaryData.totalSubletCost.add(cost_sublet); + summaryData.totalCost = summaryData.totalCost.add(costs); + + return { + id: idx, + cost_center: ccVal, + sale_labor: sale_labor && sale_labor.toFormat(), + sale_labor_dinero: sale_labor, + sale_parts: sale_parts && sale_parts.toFormat(), + sale_parts_dinero: sale_parts, + sale_additional: sale_additional && sale_additional.toFormat(), + sale_additional_dinero: sale_additional, + sale_sublet: sale_sublet && sale_sublet.toFormat(), + sale_sublet_dinero: sale_sublet, + sales: totalSales.toFormat(), + sales_dinero: totalSales, + cost_parts: cost_parts && cost_parts.toFormat(), + cost_parts_dinero: cost_parts, + cost_labor: cost_labor && cost_labor.toFormat(), + cost_labor_dinero: cost_labor, + cost_additional: cost_additional && cost_additional.toFormat(), + cost_additional_dinero: cost_additional, + cost_sublet: cost_sublet && cost_sublet.toFormat(), + cost_sublet_dinero: cost_sublet, + costs: costs.toFormat(), + costs_dinero: costs, + gpdollars_dinero: gpdollars, + gpdollars: gpdollars.toFormat(), + gppercent: formatGpPercent(gppercent), + }; }); - //Calculate thte total gross profit percentages. - - res.status(200).json({ - allCostCenterData: finalCostingdata, - allSummaryData: multiSummary.summaryData, - data: ret, - }); - } catch (error) { - logger.log("job-costing-multi-error", "ERROR", req.user.email, [jobids], { - message: error.message, - stack: error.stack, - }); - res.status(400).send(error); - } -} - -function GenerateCostingData(job) { - const defaultProfits = - job.bodyshop.md_responsibility_centers.defaults.profits; - const allCenters = _.union( - job.bodyshop.md_responsibility_centers.profits.map((p) => p.name), - job.bodyshop.md_responsibility_centers.costs.map((p) => p.name), - ["Unknown"] - ); - - const materialsHours = { mapaHrs: 0, mashHrs: 0 }; - let hasMapaLine = false; - let hasMashLine = false; - - //Massage the data. - const jobLineTotalsByProfitCenter = - job && - job.joblines.reduce( - (acc, val) => { - //Parts Lines - if (val.db_ref === "936008") { - //If either of these DB REFs change, they also need to change in job-totals/job-costing calculations. - hasMapaLine = true; - } - if (val.db_ref === "936007") { - hasMashLine = true; - } - if (val.mod_lbr_ty) { - const laborProfitCenter = - val.profitcenter_labor || - defaultProfits[val.mod_lbr_ty] || - "Unknown"; - - if (laborProfitCenter === "Unknown") - console.log("Unknown type", val.line_desc, val.mod_lbr_ty); - - const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`; - - const laborAmount = Dinero({ - amount: Math.round((job[rateName] || 0) * 100), - }).multiply(val.mod_lb_hrs || 0); - if (!acc.labor[laborProfitCenter]) - acc.labor[laborProfitCenter] = Dinero(); - acc.labor[laborProfitCenter] = - acc.labor[laborProfitCenter].add(laborAmount); - - if ( - val.mod_lb_hrs === 0 && - val.act_price > 0 && - val.lbr_op === "OP14" - ) { - //Scenario where SGI may pay out hours using a part price. - acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add( - Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }).multiply(val.part_qty) - ); - } - - if (val.mod_lbr_ty === "LAR") { - materialsHours.mapaHrs += val.mod_lb_hrs || 0; - } - if (val.mod_lbr_ty !== "LAR") { - materialsHours.mashHrs += val.mod_lb_hrs || 0; - } - } - - if ( - val.part_type && - val.part_type !== "PAE" && - val.part_type !== "PAS" && - val.part_type !== "PASL" - ) { - const partsProfitCenter = - val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; - - if (partsProfitCenter === "Unknown") - console.log("Unknown type", val.line_desc, val.part_type); - - if (!partsProfitCenter) - console.log( - "Unknown cost/profit center mapping for parts.", - val.line_desc, - val.part_type - ); - const partsAmount = Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }) - .multiply(val.part_qty || 1) - .add( - ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || - (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); - if (!acc.parts[partsProfitCenter]) - acc.parts[partsProfitCenter] = Dinero(); - acc.parts[partsProfitCenter] = - acc.parts[partsProfitCenter].add(partsAmount); - } - if ( - val.part_type && - val.part_type !== "PAE" && - (val.part_type === "PAS" || val.part_type === "PASL") - ) { - const partsProfitCenter = - val.profitcenter_part || defaultProfits[val.part_type] || "Unknown"; - - if (partsProfitCenter === "Unknown") - console.log("Unknown type", val.line_desc, val.part_type); - - if (!partsProfitCenter) - console.log( - "Unknown cost/profit center mapping for sublet.", - val.line_desc, - val.part_type - ); - const partsAmount = Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }) - .multiply(val.part_qty || 1) - .add( - ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || - (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); - if (!acc.sublet[partsProfitCenter]) - acc.sublet[partsProfitCenter] = Dinero(); - acc.sublet[partsProfitCenter] = - acc.sublet[partsProfitCenter].add(partsAmount); - } - - //To deal with additional costs. - if (!val.part_type && !val.mod_lbr_ty) { - //Does it already have a defined profit center? - //If so, use it, otherwise try to use the same from the auto-allocate logic in IO app jobs-close-auto-allocate. - const partsProfitCenter = - val.profitcenter_part || - getAdditionalCostCenter(val, defaultProfits) || - "Unknown"; - - if (partsProfitCenter === "Unknown") { - console.log("Unknown type", val.line_desc, val.part_type); - } - const partsAmount = Dinero({ - amount: Math.round((val.act_price || 0) * 100), - }) - .multiply(val.part_qty || 1) - .add( - ((val.prt_dsmk_m && val.prt_dsmk_m !== 0) || - (val.prt_dsmk_p && val.prt_dsmk_p !== 0)) && - DiscountNotAlreadyCounted(val, job.joblines) - ? val.prt_dsmk_m - ? Dinero({ amount: Math.round(val.prt_dsmk_m * 100) }) - : Dinero({ - amount: Math.round(val.act_price * 100), - }) - .multiply(val.part_qty || 0) - .percentage(Math.abs(val.prt_dsmk_p || 0)) - .multiply(val.prt_dsmk_p > 0 ? 1 : -1) - : Dinero() - ); - - if (!acc.additional[partsProfitCenter]) - acc.additional[partsProfitCenter] = Dinero(); - acc.additional[partsProfitCenter] = - acc.additional[partsProfitCenter].add(partsAmount); - } - - return acc; - }, - { parts: {}, labor: {}, additional: {}, sublet: {} } - ); - - if (!hasMapaLine) { - if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]]) - jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero(); - jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = - jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]].add( - Dinero({ - amount: Math.round((job.rate_mapa || 0) * 100), - }).multiply(materialsHours.mapaHrs || 0) - ); - } - if (!hasMashLine) { - if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]]) - jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero(); - jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = - jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]].add( - Dinero({ - amount: Math.round((job.rate_mash || 0) * 100), - }).multiply(materialsHours.mashHrs || 0) - ); - } - - //Is it a DMS Setup? - const selectedDmsAllocationConfig = - (job.bodyshop.md_responsibility_centers.dms_defaults && - job.bodyshop.md_responsibility_centers.dms_defaults.find( - (d) => d.name === job.dms_allocation - )) || - job.bodyshop.md_responsibility_centers.defaults; - - const billTotalsByCostCenters = job.bills.reduce( - (bill_acc, bill_val) => { - //At the bill level. - bill_val.billlines.map((line_val) => { - //At the bill line level. - if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { - if ( - !bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] - ) - bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = - Dinero(); - - bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = - bill_acc[ - selectedDmsAllocationConfig.costs[line_val.cost_center] - ].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } else { - const isSubletCostCenter = - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.PAS || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.PASL; - - const isAdditionalCostCenter = - // line_val.cost_center === - // job.bodyshop.md_responsibility_centers.defaults.costs.PAS || - // line_val.cost_center === - // job.bodyshop.md_responsibility_centers.defaults.costs.PASL || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.TOW || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA || - line_val.cost_center === - job.bodyshop.md_responsibility_centers.defaults.costs.MASH; - - if (isAdditionalCostCenter) { - if (!bill_acc.additionalCosts[line_val.cost_center]) - bill_acc.additionalCosts[line_val.cost_center] = Dinero(); - - bill_acc.additionalCosts[line_val.cost_center] = - bill_acc.additionalCosts[line_val.cost_center].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } else if (isSubletCostCenter) { - if (!bill_acc.subletCosts[line_val.cost_center]) - bill_acc.subletCosts[line_val.cost_center] = Dinero(); - - bill_acc.subletCosts[line_val.cost_center] = bill_acc.subletCosts[ - line_val.cost_center - ].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } else { - if (!bill_acc[line_val.cost_center]) - bill_acc[line_val.cost_center] = Dinero(); - - bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add( - Dinero({ - amount: Math.round((line_val.actual_cost || 0) * 100), - }) - .multiply(line_val.quantity) - .multiply(bill_val.is_credit_memo ? -1 : 1) - ); - } - } - - return null; - }); - return bill_acc; - }, - { additionalCosts: {}, subletCosts: {} } - ); - - //If the hourly rates for job costing are set, add them in. - - if ( - job.bodyshop.jc_hourly_rates && - (job.bodyshop.jc_hourly_rates.mapa || - typeof job.bodyshop.jc_hourly_rates.mapa === "number" || - isNaN(job.bodyshop.jc_hourly_rates.mapa) === false) - ) { - if ( - !billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] - ) - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = Dinero(); - if (job.bodyshop.use_paint_scale_data === true) { - if (job.mixdata.length > 0) { - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = Dinero({ - amount: Math.round( - ((job.mixdata[0] && job.mixdata[0].totalliquidcost) || 0) * 100 - ), + //Push adjustments to bottom line. + if (job.adjustment_bottom_line) { + //Add to totals. + const Adjustment = Dinero({ + amount: Math.round(job.adjustment_bottom_line * 100), + }); //Need to invert, since this is being assigned as a cost. + summaryData.totalLaborSales = summaryData.totalLaborSales.add(Adjustment); + summaryData.totalSales = summaryData.totalSales.add(Adjustment); + //Add to lines. + costCenterData.push({ + id: "Adj", + cost_center: "Adjustment", + sale_labor: Adjustment.toFormat(), + sale_labor_dinero: Adjustment, + sale_parts: Dinero().toFormat(), + sale_parts_dinero: Dinero(), + sale_additional: Dinero(), + sale_additional_dinero: Dinero(), + sale_sublet: Dinero(), + sale_sublet_dinero: Dinero(), + sales: Adjustment.toFormat(), + sales_dinero: Adjustment, + cost_parts: Dinero().toFormat(), + cost_parts_dinero: Dinero(), + cost_labor: Dinero().toFormat(), //Adjustment.toFormat(), + cost_labor_dinero: Dinero(), // Adjustment, + cost_additional: Dinero(), + cost_additional_dinero: Dinero(), + cost_sublet: Dinero(), + cost_sublet_dinero: Dinero(), + costs: Dinero().toFormat(), + costs_dinero: Dinero(), + gpdollars_dinero: Dinero(), + gpdollars: Dinero().toFormat(), + gppercent: formatGpPercent(0), }); - } else { - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ].add( - Dinero({ - amount: Math.round( - (job.bodyshop.jc_hourly_rates && - job.bodyshop.jc_hourly_rates.mapa * 100) || - 0 - ), - }).multiply(materialsHours.mapaHrs) - ); - } - } else { - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ] = billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MAPA - ].add( - Dinero({ - amount: Math.round( - (job.bodyshop.jc_hourly_rates && - job.bodyshop.jc_hourly_rates.mapa * 100) || - 0 - ), - }).multiply(materialsHours.mapaHrs) - ); } - } - if (job.bodyshop.jc_hourly_rates && job.bodyshop.jc_hourly_rates.mash) { - if ( - !billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ] - ) - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ] = Dinero(); - billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ] = billTotalsByCostCenters.additionalCosts[ - job.bodyshop.md_responsibility_centers.defaults.costs.MASH - ].add( - Dinero({ - amount: Math.round( - (job.bodyshop.jc_hourly_rates && - job.bodyshop.jc_hourly_rates.mash * 100) || - 0 - ), - }).multiply(materialsHours.mashHrs) + //Final summary data massaging. + + summaryData.totalLaborGp = summaryData.totalLaborSales.subtract( + summaryData.totalLaborCost + ); + summaryData.totalLaborGppercent = ( + (summaryData.totalLaborGp.getAmount() / + summaryData.totalLaborSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalLaborGppercentFormatted = formatGpPercent( + summaryData.totalLaborGppercent ); - } - const ticketTotalsByCostCenter = job.timetickets.reduce( - (ticket_acc, ticket_val) => { - //At the invoice level. + summaryData.totalPartsGp = summaryData.totalPartsSales.subtract( + summaryData.totalPartsCost + ); + summaryData.totalPartsGppercent = ( + (summaryData.totalPartsGp.getAmount() / + summaryData.totalPartsSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalPartsGppercentFormatted = formatGpPercent( + summaryData.totalPartsGppercent + ); + summaryData.totalAdditionalGp = summaryData.totalAdditionalSales.subtract( + summaryData.totalAdditionalCost + ); + summaryData.totalAdditionalGppercent = ( + (summaryData.totalAdditionalGp.getAmount() / + summaryData.totalAdditionalSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalAdditionalGppercentFormatted = formatGpPercent( + summaryData.totalAdditionalGppercent + ); + summaryData.totalSubletGp = summaryData.totalSubletSales.subtract( + summaryData.totalSubletCost + ); + summaryData.totalSubletGppercent = ( + (summaryData.totalSubletGp.getAmount() / + summaryData.totalSubletSales.getAmount()) * + 100 + ).toFixed(1); + summaryData.totalSubletGppercentFormatted = formatGpPercent( + summaryData.totalSubletGppercent + ); - if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) { - if ( - !ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] - ) - ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = - Dinero(); - - ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = - ticket_acc[ - selectedDmsAllocationConfig.costs[ticket_val.ciecacode] - ].add( - Dinero({ - amount: Math.round((ticket_val.rate || 0) * 100), - }).multiply( - ticket_val.flat_rate - ? ticket_val.productivehrs || ticket_val.actualhrs || 0 - : ticket_val.actualhrs || ticket_val.productivehrs || 0 - ) //Should base this on the employee. - ); - } else { - if (!ticket_acc[ticket_val.cost_center]) - ticket_acc[ticket_val.cost_center] = Dinero(); - - ticket_acc[ticket_val.cost_center] = ticket_acc[ - ticket_val.cost_center - ].add( - Dinero({ - amount: Math.round((ticket_val.rate || 0) * 100), - }).multiply( - ticket_val.flat_rate - ? ticket_val.productivehrs || ticket_val.actualhrs || 0 - : ticket_val.actualhrs || ticket_val.productivehrs || 0 - ) //Should base this on the employee. - ); - } - - return ticket_acc; - }, - {} - ); - - const summaryData = { - totalLaborSales: Dinero({ amount: 0 }), - totalPartsSales: Dinero({ amount: 0 }), - totalAdditionalSales: Dinero({ amount: 0 }), - totalSubletSales: Dinero({ amount: 0 }), - totalSales: Dinero({ amount: 0 }), - totalLaborCost: Dinero({ amount: 0 }), - totalPartsCost: Dinero({ amount: 0 }), - totalAdditionalCost: Dinero({ amount: 0 }), - totalSubletCost: Dinero({ amount: 0 }), - totalCost: Dinero({ amount: 0 }), - totalLaborGp: Dinero({ amount: 0 }), - totalPartsGp: Dinero({ amount: 0 }), - totalAdditionalGp: Dinero({ amount: 0 }), - totalSubletGp: Dinero({ amount: 0 }), - gpdollars: Dinero({ amount: 0 }), - totalLaborGppercent: null, - totalLaborGppercentFormatted: null, - totalPartsGppercent: null, - totalPartsGppercentFormatted: null, - totalAdditionalGppercent: null, - totalAdditionalGppercentFormatted: null, - totalSubletGppercent: null, - totalSubletGppercentFormatted: null, - gppercent: null, - gppercentFormatted: null, - }; - - const costCenterData = allCenters.map((key, idx) => { - const ccVal = key; // defaultProfits[key]; - const sale_labor = - jobLineTotalsByProfitCenter.labor[ccVal] || Dinero({ amount: 0 }); - const sale_parts = - jobLineTotalsByProfitCenter.parts[ccVal] || Dinero({ amount: 0 }); - const sale_additional = - jobLineTotalsByProfitCenter.additional[ccVal] || Dinero({ amount: 0 }); - const sale_sublet = - jobLineTotalsByProfitCenter.sublet[ccVal] || Dinero({ amount: 0 }); - - const cost_labor = ticketTotalsByCostCenter[ccVal] || Dinero({ amount: 0 }); - const cost_parts = billTotalsByCostCenters[ccVal] || Dinero({ amount: 0 }); - const cost_additional = - billTotalsByCostCenters.additionalCosts[ccVal] || Dinero({ amount: 0 }); - const cost_sublet = - billTotalsByCostCenters.subletCosts[ccVal] || Dinero({ amount: 0 }); - - const costs = cost_labor - .add(cost_parts) - .add(cost_additional) - .add(cost_sublet); - const totalSales = sale_labor - .add(sale_parts) - .add(sale_additional) - .add(sale_sublet); - const gpdollars = totalSales.subtract(costs); - const gppercent = ( - (gpdollars.getAmount() / Math.abs(totalSales.getAmount())) * - 100 + summaryData.gpdollars = summaryData.totalSales.subtract( + summaryData.totalCost + ); + summaryData.gppercent = ( + (summaryData.gpdollars.getAmount() / + Math.abs(summaryData.totalSales.getAmount())) * + 100 ).toFixed(1); - //Push summary data to avoid extra loop. - summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor); - summaryData.totalPartsSales = summaryData.totalPartsSales.add(sale_parts); - summaryData.totalAdditionalSales = - summaryData.totalAdditionalSales.add(sale_additional); - summaryData.totalSubletSales = - summaryData.totalSubletSales.add(sale_sublet); - summaryData.totalSales = summaryData.totalSales.add(totalSales); - summaryData.totalLaborCost = summaryData.totalLaborCost.add(cost_labor); - summaryData.totalPartsCost = summaryData.totalPartsCost.add(cost_parts); - summaryData.totalAdditionalCost = - summaryData.totalAdditionalCost.add(cost_additional); - summaryData.totalSubletCost = summaryData.totalSubletCost.add(cost_sublet); - summaryData.totalCost = summaryData.totalCost.add(costs); + if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0; + else if (!isFinite(summaryData.gppercent)) + summaryData.gppercentFormatted = "- ∞"; + else { + summaryData.gppercentFormatted = `${summaryData.gppercent}%`; + } - return { - id: idx, - cost_center: ccVal, - sale_labor: sale_labor && sale_labor.toFormat(), - sale_labor_dinero: sale_labor, - sale_parts: sale_parts && sale_parts.toFormat(), - sale_parts_dinero: sale_parts, - sale_additional: sale_additional && sale_additional.toFormat(), - sale_additional_dinero: sale_additional, - sale_sublet: sale_sublet && sale_sublet.toFormat(), - sale_sublet_dinero: sale_sublet, - sales: totalSales.toFormat(), - sales_dinero: totalSales, - cost_parts: cost_parts && cost_parts.toFormat(), - cost_parts_dinero: cost_parts, - cost_labor: cost_labor && cost_labor.toFormat(), - cost_labor_dinero: cost_labor, - cost_additional: cost_additional && cost_additional.toFormat(), - cost_additional_dinero: cost_additional, - cost_sublet: cost_sublet && cost_sublet.toFormat(), - cost_sublet_dinero: cost_sublet, - costs: costs.toFormat(), - costs_dinero: costs, - gpdollars_dinero: gpdollars, - gpdollars: gpdollars.toFormat(), - gppercent: formatGpPercent(gppercent), - }; - }); - - //Push adjustments to bottom line. - if (job.adjustment_bottom_line) { - //Add to totals. - const Adjustment = Dinero({ - amount: Math.round(job.adjustment_bottom_line * 100), - }); //Need to invert, since this is being assigned as a cost. - summaryData.totalLaborSales = summaryData.totalLaborSales.add(Adjustment); - summaryData.totalSales = summaryData.totalSales.add(Adjustment); - //Add to lines. - costCenterData.push({ - id: "Adj", - cost_center: "Adjustment", - sale_labor: Adjustment.toFormat(), - sale_labor_dinero: Adjustment, - sale_parts: Dinero().toFormat(), - sale_parts_dinero: Dinero(), - sale_additional: Dinero(), - sale_additional_dinero: Dinero(), - sale_sublet: Dinero(), - sale_sublet_dinero: Dinero(), - sales: Adjustment.toFormat(), - sales_dinero: Adjustment, - cost_parts: Dinero().toFormat(), - cost_parts_dinero: Dinero(), - cost_labor: Dinero().toFormat(), //Adjustment.toFormat(), - cost_labor_dinero: Dinero(), // Adjustment, - cost_additional: Dinero(), - cost_additional_dinero: Dinero(), - cost_sublet: Dinero(), - cost_sublet_dinero: Dinero(), - costs: Dinero().toFormat(), - costs_dinero: Dinero(), - gpdollars_dinero: Dinero(), - gpdollars: Dinero().toFormat(), - gppercent: formatGpPercent(0), - }); - } - - //Final summary data massaging. - - summaryData.totalLaborGp = summaryData.totalLaborSales.subtract( - summaryData.totalLaborCost - ); - summaryData.totalLaborGppercent = ( - (summaryData.totalLaborGp.getAmount() / - summaryData.totalLaborSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalLaborGppercentFormatted = formatGpPercent( - summaryData.totalLaborGppercent - ); - - summaryData.totalPartsGp = summaryData.totalPartsSales.subtract( - summaryData.totalPartsCost - ); - summaryData.totalPartsGppercent = ( - (summaryData.totalPartsGp.getAmount() / - summaryData.totalPartsSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalPartsGppercentFormatted = formatGpPercent( - summaryData.totalPartsGppercent - ); - summaryData.totalAdditionalGp = summaryData.totalAdditionalSales.subtract( - summaryData.totalAdditionalCost - ); - summaryData.totalAdditionalGppercent = ( - (summaryData.totalAdditionalGp.getAmount() / - summaryData.totalAdditionalSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalAdditionalGppercentFormatted = formatGpPercent( - summaryData.totalAdditionalGppercent - ); - summaryData.totalSubletGp = summaryData.totalSubletSales.subtract( - summaryData.totalSubletCost - ); - summaryData.totalSubletGppercent = ( - (summaryData.totalSubletGp.getAmount() / - summaryData.totalSubletSales.getAmount()) * - 100 - ).toFixed(1); - summaryData.totalSubletGppercentFormatted = formatGpPercent( - summaryData.totalSubletGppercent - ); - - summaryData.gpdollars = summaryData.totalSales.subtract( - summaryData.totalCost - ); - summaryData.gppercent = ( - (summaryData.gpdollars.getAmount() / - Math.abs(summaryData.totalSales.getAmount())) * - 100 - ).toFixed(1); - - if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0; - else if (!isFinite(summaryData.gppercent)) - summaryData.gppercentFormatted = "- ∞"; - else { - summaryData.gppercentFormatted = `${summaryData.gppercent}%`; - } - - return { summaryData, costCenterData }; + return {summaryData, costCenterData}; } exports.JobCosting = JobCosting; exports.JobCostingMulti = JobCostingMulti; const formatGpPercent = (gppercent) => { - let gppercentFormatted; - if (isNaN(gppercent)) gppercentFormatted = "0%"; - else if (!isFinite(gppercent)) gppercentFormatted = "- ∞"; - else { - gppercentFormatted = `${gppercent}%`; - } + let gppercentFormatted; + if (isNaN(gppercent)) gppercentFormatted = "0%"; + else if (!isFinite(gppercent)) gppercentFormatted = "- ∞"; + else { + gppercentFormatted = `${gppercent}%`; + } - return gppercentFormatted; + return gppercentFormatted; }; //Verify that this stays in line with jobs-close-auto-allocate logic from the application. const getAdditionalCostCenter = (jl, profitCenters) => { - if (!jl.part_type && !jl.mod_lbr_ty) { - const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : ""; + if (!jl.part_type && !jl.mod_lbr_ty) { + const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : ""; - if (lineDesc.includes("shop mat")) { - return profitCenters["MASH"]; - } else if (lineDesc.includes("paint/mat")) { - return profitCenters["MAPA"]; - } else if (lineDesc.includes("ats amount")) { - return profitCenters["ATS"]; - } else if (lineDesc.includes("towing")) { - return profitCenters["TOW"]; - } else { - return null; + if (lineDesc.includes("shop mat")) { + return profitCenters["MASH"]; + } else if (lineDesc.includes("paint/mat")) { + return profitCenters["MAPA"]; + } else if (lineDesc.includes("ats amount")) { + return profitCenters["ATS"]; + } else if (lineDesc.includes("towing")) { + return profitCenters["TOW"]; + } else { + return null; + } } - } }; diff --git a/server/job/job-lifecycle.js b/server/job/job-lifecycle.js new file mode 100644 index 000000000..c2ea72737 --- /dev/null +++ b/server/job/job-lifecycle.js @@ -0,0 +1,68 @@ +const _ = require("lodash"); +const queries = require("../graphql-client/queries"); +const moment = require("moment"); +const durationToHumanReadable = require("../utils/durationToHumanReadable"); +const calculateStatusDuration = require("../utils/calculateStatusDuration"); + +const jobLifecycle = async (req, res) => { + // Grab the jobids and statuses from the request body + const { + jobids, + statuses + } = req.body; + + if (!jobids) { + return res.status(400).json({ + error: "Missing jobids" + }); + } + + const jobIDs = _.isArray(jobids) ? jobids : [jobids]; + const client = req.userGraphQLClient; + const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, {jobids: jobIDs,}); + + const transitions = resp.transitions; + + if (!transitions) { + return res.status(200).json({ + jobIDs, + transitions: [] + }); + + } + + const transitionsByJobId = _.groupBy(resp.transitions, 'jobid'); + + const groupedTransitions = {}; + + for (let jobId in transitionsByJobId) { + let lifecycle = transitionsByJobId[jobId].map(transition => { + transition.start_readable = transition.start ? moment(transition.start).fromNow() : 'N/A'; + transition.end_readable = transition.end ? moment(transition.end).fromNow() : 'N/A'; + + if (transition.duration) { + transition.duration_seconds = Math.round(transition.duration / 1000); + transition.duration_minutes = Math.round(transition.duration_seconds / 60); + let duration = moment.duration(transition.duration); + transition.duration_readable = durationToHumanReadable(duration); + } else { + transition.duration_seconds = 0; + transition.duration_minutes = 0; + transition.duration_readable = 'N/A'; + } + return transition; + }); + + groupedTransitions[jobId] = { + lifecycle: lifecycle, + durations: calculateStatusDuration(lifecycle, statuses), + }; + } + + return res.status(200).json({ + jobIDs, + transition: groupedTransitions, + }); +} + +module.exports = jobLifecycle; \ No newline at end of file diff --git a/server/job/job-status-transition.js b/server/job/job-status-transition.js index fd74a2ec3..d97249c06 100644 --- a/server/job/job-status-transition.js +++ b/server/job/job-status-transition.js @@ -9,24 +9,25 @@ const logger = require("../utils/logger"); Dinero.globalRoundingMode = "HALF_EVEN"; const path = require("path"); const client = require("../graphql-client/graphql-client").client; + require("dotenv").config({ path: path.resolve( process.cwd(), `.env.${process.env.NODE_ENV || "development"}` ), }); + async function StatusTransition(req, res) { - if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { - res.status(401).send("Unauthorized"); - return; - } - res.sendStatus(200); - return; const { id: jobid, status: value, shopid: bodyshopid, } = req.body.event.data.new; + + // Create record OPEN on new item, enter state + // If change to SCHEDULE, update the last record and create a new record (update status and end time on old record, create a new record saying we came from previous status going to previous status + // (Timeline) + // Final status is exported, there is no end date as there is no further transition (has no end date) try { const { update_transitions } = await client.request( queries.UPDATE_OLD_TRANSITION, @@ -47,27 +48,36 @@ async function StatusTransition(req, res) { : new Date(update_transitions.returning[0].end) - new Date(update_transitions.returning[0].start); - const resp2 = await client.request(queries.INSERT_NEW_TRANSITION, { - oldTransitionId: - update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].id, - duration, - newTransition: { - bodyshopid: bodyshopid, - jobid: jobid, - start: - update_transitions.affected_rows === 0 - ? new Date() - : update_transitions.returning[0].end, - prev_value: - update_transitions.affected_rows === 0 - ? null - : update_transitions.returning[0].value, - value: value, - type: "status", - }, - }); + const resp2 = await client.request( + queries.INSERT_NEW_TRANSITION(update_transitions.affected_rows > 0), + { + ...(update_transitions.affected_rows > 0 + ? { + oldTransitionId: + update_transitions.affected_rows === 0 + ? null + : update_transitions.returning[0].id, + duration, + } + : {}), + newTransition: { + bodyshopid: bodyshopid, + jobid: jobid, + start: + update_transitions.affected_rows === 0 + ? new Date() + : update_transitions.returning[0].end, + prev_value: + update_transitions.affected_rows === 0 + ? null + : update_transitions.returning[0].value, + value: value, + type: "status", + }, + } + ); + + logger.log("job-transition-update-result", "DEBUG", null, jobid, resp2); //Check to see if there is an existing status transition record. //Query using Job ID, start is not null, end is null. diff --git a/server/job/job-totals.js b/server/job/job-totals.js index 35246ec8c..b942871cf 100644 --- a/server/job/job-totals.js +++ b/server/job/job-totals.js @@ -1,20 +1,18 @@ const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); +const logger = require('../utils/logger'); + // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; Dinero.globalRoundingMode = "HALF_EVEN"; exports.totalsSsu = async function (req, res) { - const BearerToken = req.headers.authorization; const { id } = req.body; + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); try { const job = await client @@ -75,21 +73,19 @@ async function TotalsServerSide(req, res) { } async function Totals(req, res) { - const { job } = req.body; + const { job, id } = req.body; + + const logger = req.logger; + const client = req.userGraphQLClient; + logger.log("job-totals", "DEBUG", req.user.email, job.id, { jobid: job.id, }); - const BearerToken = req.headers.authorization; - const { id } = req.body; logger.log("job-totals-ssu", "DEBUG", req.user.email, id, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); await AutoAddAtsIfRequired({ job, client }); + try { let ret = { parts: CalculatePartsTotals(job.joblines), diff --git a/server/job/job.js b/server/job/job.js index e0fff1695..f21cbf4f4 100644 --- a/server/job/job.js +++ b/server/job/job.js @@ -3,3 +3,4 @@ exports.totalsSsu = require("./job-totals").totalsSsu; exports.costing = require("./job-costing").JobCosting; exports.costingmulti = require("./job-costing").JobCostingMulti; exports.statustransition = require("./job-status-transition").statustransition; +exports.lifecycle = require('./job-lifecycle'); \ No newline at end of file diff --git a/server/middleware/eventAuthorizationMIddleware.js b/server/middleware/eventAuthorizationMIddleware.js new file mode 100644 index 000000000..9dd4dfd3a --- /dev/null +++ b/server/middleware/eventAuthorizationMIddleware.js @@ -0,0 +1,20 @@ +const path = require("path"); + +/** + * Checks if the event secret is correct + * It adds the following properties to the request object: + * - req.isEventAuthorized - Returns true if the event secret is correct + * @param req + * @param res + * @param next + */ +function eventAuthorizationMiddleware(req, res, next) { + if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { + return res.status(401).send("Unauthorized"); + } + + req.isEventAuthorized = true; + next(); +} + +module.exports = eventAuthorizationMiddleware; \ No newline at end of file diff --git a/server/middleware/validateAdminMiddleware.js b/server/middleware/validateAdminMiddleware.js new file mode 100644 index 000000000..cfd53b171 --- /dev/null +++ b/server/middleware/validateAdminMiddleware.js @@ -0,0 +1,26 @@ +const logger = require("../utils/logger"); +const adminEmail = require("../utils/adminEmail"); + +/** + * Validate admin middleware + * It adds the following properties to the request object: + * - req.isAdmin - returns true if the user passed an admin check + * @param req + * @param res + * @param next + * @returns {*} + */ +const validateAdminMiddleware = (req, res, next) => { + if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) { + logger.log("admin-validation-failed", "ERROR", req.user.email, null, { + request: req.body, + user: req.user, + }); + return res.sendStatus(404); + } + + req.isAdmin = true; + next(); +}; + +module.exports = validateAdminMiddleware; \ No newline at end of file diff --git a/server/middleware/validateFirebaseIdTokenMiddleware.js b/server/middleware/validateFirebaseIdTokenMiddleware.js new file mode 100644 index 000000000..53d1cc775 --- /dev/null +++ b/server/middleware/validateFirebaseIdTokenMiddleware.js @@ -0,0 +1,69 @@ +const logger = require("../utils/logger"); +const admin = require("firebase-admin"); + +/** + * Middleware to validate Firebase ID Tokens. + * This middleware is used to protect API endpoints from unauthorized access. + * It adds the following properties to the request object: + * - req.user - the decoded Firebase ID Token + * @param req + * @param res + * @param next + * @returns {Promise} + */ +const validateFirebaseIdTokenMiddleware = async (req, res, next) => { + if ( + ( + !req.headers.authorization || + !req.headers.authorization.startsWith("Bearer ")) && + !(req.cookies && req.cookies.__session + ) + ) { + console.error("Unauthorized attempt. No authorization provided."); + return res.status(403).send("Unauthorized"); + } + + let idToken; + + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer ") + ) { + // console.log('Found "Authorization" header'); + // Read the ID Token from the Authorization header. + idToken = req.headers.authorization.split("Bearer ")[1]; + } else if (req.cookies) { + //console.log('Found "__session" cookie'); + // Read the ID Token from cookie. + idToken = req.cookies.__session; + } else { + // No cookie + console.error("Unauthorized attempt. No cookie provided."); + logger.log("api-unauthorized-call", "WARN", null, null, { + req, + type: "no-cookie", + }); + + return res.status(403).send("Unauthorized"); + } + + try { + const decodedIdToken = await admin.auth().verifyIdToken(idToken); + //console.log("ID Token correctly decoded", decodedIdToken); + req.user = decodedIdToken; + next(); + + } catch (error) { + logger.log("api-unauthorized-call", "WARN", null, null, { + path: req.path, + body: req.body, + + type: "unauthroized", + ...error, + }); + + return res.status(401).send("Unauthorized"); + } +}; + +module.exports = validateFirebaseIdTokenMiddleware; \ No newline at end of file diff --git a/server/middleware/withUserGraphQLClientMiddleware.js b/server/middleware/withUserGraphQLClientMiddleware.js new file mode 100644 index 000000000..e55b58c8d --- /dev/null +++ b/server/middleware/withUserGraphQLClientMiddleware.js @@ -0,0 +1,24 @@ +const {GraphQLClient} = require("graphql-request"); + +/** + * Middleware to add a GraphQL Client to the request object + * Adds the following to the request object: + * req.userGraphQLClient - GraphQL Client with user Bearer Token + * req.BearerToken - Bearer Token + * @param req + * @param res + * @param next + */ +const withUserGraphQLClientMiddleware = (req, res, next) => { + const BearerToken = req.headers.authorization; + req.userGraphQLClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { + headers: { + Authorization: BearerToken, + }, + }); + req.BearerToken = BearerToken; + + next(); +}; + +module.exports = withUserGraphQLClientMiddleware; \ No newline at end of file diff --git a/server/mixdata/mixdata.js b/server/mixdata/mixdata.js index a0d5141f3..41a3b0eb8 100644 --- a/server/mixdata/mixdata.js +++ b/server/mixdata/mixdata.js @@ -1,9 +1,8 @@ const path = require("path"); const _ = require("lodash"); -const logger = require("../utils/logger"); const xml2js = require("xml2js"); -const GraphQLClient = require("graphql-request").GraphQLClient; const queries = require("../graphql-client/queries"); +const logger = require('../utils/logger'); require("dotenv").config({ path: path.resolve( @@ -15,13 +14,10 @@ require("dotenv").config({ exports.mixdataUpload = async (req, res) => { const { bodyshopid } = req.body; - const BearerToken = req.headers.authorization; + const client = req.userGraphQLClient; + logger.log("job-mixdata-upload", "DEBUG", req.user.email, null, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + try { for (const element of req.files) { diff --git a/server/opensearch/os-handler.js b/server/opensearch/os-handler.js index 7cb544400..5fe63695c 100644 --- a/server/opensearch/os-handler.js +++ b/server/opensearch/os-handler.js @@ -5,7 +5,6 @@ require("dotenv").config({ ), }); -const GraphQLClient = require("graphql-request").GraphQLClient; //const client = require("../graphql-client/graphql-client").client; const logger = require("../utils/logger"); const queries = require("../graphql-client/queries"); @@ -15,10 +14,6 @@ const {getClient} = require('../../libs/awsUtils'); async function OpenSearchUpdateHandler(req, res) { - if (req.headers["event-secret"] !== process.env.EVENT_SECRET) { - res.status(401).send("Unauthorized"); - return; - } try { const osClient = await getClient(); @@ -186,12 +181,8 @@ async function OpenSearchSearchHandler(req, res) { search, }); - const BearerToken = req.headers.authorization; - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; const assocs = await client .setHeaders({Authorization: BearerToken}) diff --git a/server/parts-scan/parts-scan.js b/server/parts-scan/parts-scan.js index c5b619303..b2f51ef3b 100644 --- a/server/parts-scan/parts-scan.js +++ b/server/parts-scan/parts-scan.js @@ -1,21 +1,19 @@ const Dinero = require("dinero.js"); const queries = require("../graphql-client/queries"); +const logger = require('../utils/logger'); const { job } = require("../scheduling/scheduling-job"); -const GraphQLClient = require("graphql-request").GraphQLClient; -const logger = require("../utils/logger"); const _ = require("lodash"); + // Dinero.defaultCurrency = "USD"; // Dinero.globalLocale = "en-CA"; exports.partsScan = async function (req, res) { - const BearerToken = req.headers.authorization; const { jobid } = req.body; + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + logger.log("job-parts-scan", "DEBUG", req.user?.email, jobid, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); try { //Query all jobline data using the user's authorization. diff --git a/server/routes/accountingRoutes.js b/server/routes/accountingRoutes.js new file mode 100644 index 000000000..04576bf01 --- /dev/null +++ b/server/routes/accountingRoutes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {payments, payables, receivables} = require("../accounting/qbxml/qbxml"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/qbxml/receivables', withUserGraphQLClientMiddleware, receivables); +router.post('/qbxml/payables', withUserGraphQLClientMiddleware, payables); +router.post('/qbxml/payments', withUserGraphQLClientMiddleware, payments); + +module.exports = router; diff --git a/server/routes/adminRoutes.js b/server/routes/adminRoutes.js new file mode 100644 index 000000000..617f343b3 --- /dev/null +++ b/server/routes/adminRoutes.js @@ -0,0 +1,18 @@ +const express = require('express'); +const router = express.Router(); +const fb = require('../firebase/firebase-handler'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {createAssociation, createShop, updateShop, updateCounter} = require("../admin/adminops"); +const validateAdminMiddleware = require("../middleware/validateAdminMiddleware"); + +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/createassociation', validateAdminMiddleware, createAssociation); +router.post('/createshop', validateAdminMiddleware, createShop); +router.post('/updateshop', validateAdminMiddleware, updateShop); +router.post('/updatecounter', validateAdminMiddleware, updateCounter); +router.post('/updateuser', fb.updateUser); +router.post('/getuser', fb.getUser); +router.post('/createuser', fb.createUser); + +module.exports = router; diff --git a/server/routes/cdkRoutes.js b/server/routes/cdkRoutes.js new file mode 100644 index 000000000..85d2b49d0 --- /dev/null +++ b/server/routes/cdkRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const cdkGetMake = require('../cdk/cdk-get-makes'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/getvehicles', withUserGraphQLClientMiddleware, cdkGetMake.default); + +module.exports = router; diff --git a/server/routes/csiRoutes.js b/server/routes/csiRoutes.js new file mode 100644 index 000000000..8f47a2b2d --- /dev/null +++ b/server/routes/csiRoutes.js @@ -0,0 +1,8 @@ +const express = require("express"); +const router = express.Router(); +const { lookup, submit } = require("../csi/csi"); + +router.post("/lookup", lookup); +router.post("/submit", submit); + +module.exports = router; diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js new file mode 100644 index 000000000..0240f3388 --- /dev/null +++ b/server/routes/dataRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const {autohouse, claimscorp, kaizen} = require('../data/data'); + +router.post('/ah', autohouse); +router.post('/cc', claimscorp); +router.post('/kaizen', kaizen); + +module.exports = router; diff --git a/server/routes/intellipayRoutes.js b/server/routes/intellipayRoutes.js new file mode 100644 index 000000000..3952a3450 --- /dev/null +++ b/server/routes/intellipayRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {lightbox_credentials, payment_refund, generate_payment_url, postback} = require("../intellipay/intellipay"); + +router.post('/lightbox_credentials', validateFirebaseIdTokenMiddleware, lightbox_credentials); +router.post('/payment_refund', validateFirebaseIdTokenMiddleware, payment_refund); +router.post('/generate_payment_url', validateFirebaseIdTokenMiddleware, generate_payment_url); +router.post('/postback', postback); + +module.exports = router; diff --git a/server/routes/jobRoutes.js b/server/routes/jobRoutes.js new file mode 100644 index 000000000..9b4a5e9f6 --- /dev/null +++ b/server/routes/jobRoutes.js @@ -0,0 +1,18 @@ +const express = require('express'); +const router = express.Router(); +const job = require('../job/job'); +const {partsScan} = require('../parts-scan/parts-scan'); +const eventAuthorizationMiddleware = require('../middleware/eventAuthorizationMIddleware'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {totals, statustransition, totalsSsu, costing, lifecycle, costingmulti} = require("../job/job"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +router.post('/totals', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals); +router.post('/statustransition', eventAuthorizationMiddleware, statustransition); +router.post('/totalsssu', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,totalsSsu); +router.post('/costing', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware,costing); +router.post('/lifecycle', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, lifecycle); +router.post('/costingmulti', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti); +router.post('/partsscan', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan); + +module.exports = router; diff --git a/server/routes/mediaRoutes.js b/server/routes/mediaRoutes.js new file mode 100644 index 000000000..a44a1a048 --- /dev/null +++ b/server/routes/mediaRoutes.js @@ -0,0 +1,13 @@ +const express = require('express'); +const router = express.Router(); +const {createSignedUploadURL, downloadFiles, renameKeys, deleteFiles} = require('../media/media'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); + +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/sign', createSignedUploadURL); +router.post('/download', downloadFiles); +router.post('/rename', renameKeys); +router.post('/delete', deleteFiles); + +module.exports = router; diff --git a/server/routes/miscellaneousRoutes.js b/server/routes/miscellaneousRoutes.js new file mode 100644 index 000000000..9d20d9b42 --- /dev/null +++ b/server/routes/miscellaneousRoutes.js @@ -0,0 +1,51 @@ +const express = require('express'); +const router = express.Router(); +const logger = require("../../server/utils/logger"); +const sendEmail = require("../email/sendemail"); +const data = require("../data/data"); +const bodyParser = require("body-parser"); +const ioevent = require("../ioevent/ioevent"); +const taskHandler = require("../tasks/tasks"); +const os = require("../opensearch/os-handler"); +const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware"); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +//Test route to ensure Express is responding. +router.get("/test", async function (req, res) { + const commit = require("child_process").execSync( + "git rev-parse --short HEAD" + ); + // console.log(app.get('trust proxy')); + // console.log("remoteAddress", req.socket.remoteAddress); + // console.log("X-Forwarded-For", req.header('x-forwarded-for')); + logger.log("test-api-status", "DEBUG", "api", {commit}); + // sendEmail.sendServerEmail({ + // subject: `API Check - ${process.env.NODE_ENV}`, + // text: `Server API check has come in. Remote IP: ${req.socket.remoteAddress}, X-Forwarded-For: ${req.header('x-forwarded-for')}`, + // }); + sendEmail.sendServerEmail({ + subject: `API Check - ${process.env.NODE_ENV}`, + text: `Server API check has come in.`, + }); + res.status(200).send(`OK - ${commit}`); +}); + +// Search +router.post("/search", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, os.search); +router.post("/opensearch", eventAuthorizationMiddleware, os.handler); + + +// IO Events +router.post('/ioevent', ioevent.default); + +// Email +router.post('/sendemail', validateFirebaseIdTokenMiddleware, sendEmail.sendEmail); +router.post('/emailbounce', bodyParser.text(), sendEmail.emailBounce); + +// Handlers +router.post('/record-handler/arms', data.arms); +router.post("/taskHandler", validateFirebaseIdTokenMiddleware, taskHandler.taskHandler); + + +module.exports = router; diff --git a/server/routes/mixDataRoutes.js b/server/routes/mixDataRoutes.js new file mode 100644 index 000000000..b9ac289e7 --- /dev/null +++ b/server/routes/mixDataRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const multer = require('multer'); +const upload = multer(); +const {mixdataUpload} = require('../mixdata/mixdata'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +router.post('/upload', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, upload.any(), mixdataUpload); + +module.exports = router; diff --git a/server/routes/notificationsRoutes.js b/server/routes/notificationsRoutes.js new file mode 100644 index 000000000..1a8e9de7b --- /dev/null +++ b/server/routes/notificationsRoutes.js @@ -0,0 +1,11 @@ +const express = require('express'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const {subscribe, unsubscribe} = require("../firebase/firebase-handler"); +const router = express.Router(); + +router.use(validateFirebaseIdTokenMiddleware); + +router.post('/subscribe', subscribe); +router.post('/unsubscribe', unsubscribe); + +module.exports = router; diff --git a/server/routes/qboRoutes.js b/server/routes/qboRoutes.js new file mode 100644 index 000000000..22b54e23e --- /dev/null +++ b/server/routes/qboRoutes.js @@ -0,0 +1,14 @@ +const express = require('express'); +const router = express.Router(); +const {authorize, callback, receivables, payables, payments} = require('../accounting/qbo/qbo'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); // Assuming you have a qbo module for handling QuickBooks Online related functionalities + +// Define the routes for QuickBooks Online +router.post('/authorize', validateFirebaseIdTokenMiddleware, authorize); +router.get('/callback', callback); +router.post('/receivables', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, receivables); +router.post('/payables', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, payables); +router.post('/payments', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, payments); + +module.exports = router; diff --git a/server/routes/renderRoutes.js b/server/routes/renderRoutes.js new file mode 100644 index 000000000..7242404e5 --- /dev/null +++ b/server/routes/renderRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const {inlinecss} = require('../render/inlinecss'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); + +// Define the route for inline CSS rendering +router.post('/inlinecss', validateFirebaseIdTokenMiddleware, inlinecss); + +module.exports = router; diff --git a/server/routes/schedulingRoutes.js b/server/routes/schedulingRoutes.js new file mode 100644 index 000000000..816114315 --- /dev/null +++ b/server/routes/schedulingRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const {job} = require('../scheduling/scheduling-job'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); +const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware"); + +router.post('/job', validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, job); + +module.exports = router; diff --git a/server/routes/smsRoutes.js b/server/routes/smsRoutes.js new file mode 100644 index 000000000..9952e0d64 --- /dev/null +++ b/server/routes/smsRoutes.js @@ -0,0 +1,17 @@ +const express = require('express'); +const router = express.Router(); +const twilio = require('twilio'); +const {receive} = require('../sms/receive'); +const {send} = require('../sms/send'); +const {status, markConversationRead} = require('../sms/status'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); + +// Twilio Webhook Middleware for production +const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" }); + +router.post('/receive', twilioWebhookMiddleware, receive); +router.post('/send', validateFirebaseIdTokenMiddleware, send); +router.post('/status', twilioWebhookMiddleware, status); +router.post('/markConversationRead', validateFirebaseIdTokenMiddleware, markConversationRead); + +module.exports = router; diff --git a/server/routes/techRoutes.js b/server/routes/techRoutes.js new file mode 100644 index 000000000..e7594f532 --- /dev/null +++ b/server/routes/techRoutes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const {techLogin} = require('../tech/tech'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); + +router.post('/login', validateFirebaseIdTokenMiddleware, techLogin); + +module.exports = router; diff --git a/server/routes/utilRoutes.js b/server/routes/utilRoutes.js new file mode 100644 index 000000000..938b1e1b1 --- /dev/null +++ b/server/routes/utilRoutes.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const {servertime, jsrAuth} = require('../utils/utils'); +const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware"); + +router.post('/time', servertime); +router.post('/jsr', validateFirebaseIdTokenMiddleware, jsrAuth); + +module.exports = router; diff --git a/server/scheduling/scheduling-job.js b/server/scheduling/scheduling-job.js index e906c866e..178ef3c73 100644 --- a/server/scheduling/scheduling-job.js +++ b/server/scheduling/scheduling-job.js @@ -1,4 +1,3 @@ -const GraphQLClient = require("graphql-request").GraphQLClient; const path = require("path"); const queries = require("../graphql-client/queries"); const Dinero = require("dinero.js"); @@ -14,17 +13,14 @@ require("dotenv").config({ }); exports.job = async (req, res) => { - const BearerToken = req.headers.authorization; const { jobId } = req.body; + + const BearerToken = req.BearerToken; + const client = req.userGraphQLClient; + try { logger.log("smart-scheduling-start", "DEBUG", req.user.email, jobId, null); - const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, { - headers: { - Authorization: BearerToken, - }, - }); - const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_UPCOMING_APPOINTMENTS, { diff --git a/server/utils/adminEmail.js b/server/utils/adminEmail.js new file mode 100644 index 000000000..70c44cb47 --- /dev/null +++ b/server/utils/adminEmail.js @@ -0,0 +1,13 @@ +/** + * List of admin email addresses + * @type {string[]} + */ +const adminEmail = [ + "patrick@imex.dev", + //"patrick@imex.test", + "patrick@imex.prod", + "patrick@imexsystems.ca", + "patrick@thinkimex.com", +]; + +module.exports = adminEmail; \ No newline at end of file diff --git a/server/utils/calculateStatusDuration.js b/server/utils/calculateStatusDuration.js new file mode 100644 index 000000000..16165d001 --- /dev/null +++ b/server/utils/calculateStatusDuration.js @@ -0,0 +1,100 @@ +const durationToHumanReadable = require("./durationToHumanReadable"); +const moment = require("moment"); +const _ = require("lodash"); +const crypto = require('crypto'); + +const getColor = (key) => { + const hash = crypto.createHash('sha256'); + hash.update(key); + const hashedKey = hash.digest('hex'); + const num = parseInt(hashedKey, 16); + return '#' + (num % 16777215).toString(16).padStart(6, '0'); +}; + +const calculateStatusDuration = (transitions, statuses) => { + let statusDuration = {}; + let totalDuration = 0; + let totalCurrentStatusDuration = null; + let summations = []; + + transitions.forEach((transition, index) => { + let duration = transition.duration; + totalDuration += duration; + if (transition.start && !transition.end) { + const startMoment = moment(transition.start); + const nowMoment = moment(); + const duration = moment.duration(nowMoment.diff(startMoment)); + totalCurrentStatusDuration = { + value: duration.asMilliseconds(), + humanReadable: durationToHumanReadable(duration) + }; + } + + if (!transition.prev_value) { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } else if (!transition.next_value) { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } else { + if (statusDuration[transition.value]) { + statusDuration[transition.value].value += duration; + statusDuration[transition.value].humanReadable = transition.duration_readable; + } else { + statusDuration[transition.value] = { + value: duration, + humanReadable: transition.duration_readable + }; + } + } + }); + + // Calculate the percentage for each status +// Calculate the percentage for each status + let totalPercentage = 0; + const statusKeys = Object.keys(statusDuration); + statusKeys.forEach((status, index) => { + if (index !== statusKeys.length - 1) { + const percentage = (statusDuration[status].value / totalDuration) * 100; + totalPercentage += percentage; + statusDuration[status].percentage = percentage; + } else { + statusDuration[status].percentage = 100 - totalPercentage; + } + }); + + for (let [status, {value, humanReadable}] of Object.entries(statusDuration)) { + if (status !== 'total') { + summations.push({ + status, + value, + humanReadable, + percentage: statusDuration[status].percentage, + color: getColor(status), + roundedPercentage: `${Math.round(statusDuration[status].percentage)}%` + }); + } + } + + const humanReadableTotal = durationToHumanReadable(moment.duration(totalDuration)); + + return { + summations: _.isArray(statuses) && !_.isEmpty(statuses) ? summations.sort((a, b) => { + return statuses.indexOf(a.status) - statuses.indexOf(b.status); + }) : summations, + totalStatuses: summations.length, + total: totalDuration, + totalCurrentStatusDuration, + humanReadableTotal + }; +} +module.exports = calculateStatusDuration; \ No newline at end of file diff --git a/server/utils/durationToHumanReadable.js b/server/utils/durationToHumanReadable.js new file mode 100644 index 000000000..e6820c9bf --- /dev/null +++ b/server/utils/durationToHumanReadable.js @@ -0,0 +1,22 @@ +const durationToHumanReadable = (duration) => { + if (!duration) return 'N/A'; + + let parts = []; + + let years = duration.years(); + let months = duration.months(); + let days = duration.days(); + let hours = duration.hours(); + let minutes = duration.minutes(); + let seconds = duration.seconds(); + + if (years) parts.push(years + ' year' + (years > 1 ? 's' : '')); + if (months) parts.push(months + ' month' + (months > 1 ? 's' : '')); + if (days) parts.push(days + ' day' + (days > 1 ? 's' : '')); + if (hours) parts.push(hours + ' hour' + (hours > 1 ? 's' : '')); + if (minutes) parts.push(minutes + ' minute' + (minutes > 1 ? 's' : '')); + if (seconds) parts.push(seconds + ' second' + (seconds > 1 ? 's' : '')); + + return parts.join(', '); +} +module.exports = durationToHumanReadable; \ No newline at end of file