Compare commits
3 Commits
master
...
feature/cy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345fc48c17 | ||
|
|
b8fe566030 | ||
|
|
bee078fe18 |
@@ -8,13 +8,13 @@ orbs:
|
|||||||
jobs:
|
jobs:
|
||||||
api-deploy:
|
api-deploy:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: "cimg/base:stable"
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- eb/setup
|
- eb/setup
|
||||||
- run:
|
- run:
|
||||||
command: |
|
command: |
|
||||||
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
|
eb init imex-online-production-api -r ca-central-1 -p "Node.js 16 running on 64bit Amazon Linux 2"
|
||||||
eb status --verbose
|
eb status --verbose
|
||||||
eb deploy
|
eb deploy
|
||||||
eb status
|
eb status
|
||||||
@@ -42,22 +42,31 @@ jobs:
|
|||||||
app-build:
|
app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.15.0
|
- image: cimg/node:16.15.0
|
||||||
resource_class: large
|
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
path: ~/repo
|
path: ~/repo
|
||||||
|
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore Yarn Package Cache
|
||||||
|
keys:
|
||||||
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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
|
||||||
|
|
||||||
- run: npm run build
|
- run: yarn run build
|
||||||
|
|
||||||
- aws-s3/sync:
|
- aws-s3/sync:
|
||||||
from: build
|
from: build
|
||||||
to: "s3://imex-online-production/"
|
to: "s3://imex-online-production/"
|
||||||
arguments: "--exclude '*.map'"
|
|
||||||
- jira/notify
|
- jira/notify
|
||||||
|
|
||||||
test-hasura-migrate:
|
test-hasura-migrate:
|
||||||
@@ -83,22 +92,31 @@ jobs:
|
|||||||
test-app-build:
|
test-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:16.15.0
|
- image: cimg/node:16.15.0
|
||||||
resource_class: large
|
|
||||||
working_directory: ~/repo/client
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
path: ~/repo
|
path: ~/repo
|
||||||
|
|
||||||
|
- restore_cache:
|
||||||
|
name: Restore Yarn Package Cache
|
||||||
|
keys:
|
||||||
|
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: npm i
|
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
|
||||||
|
|
||||||
- run: npm run build:test
|
- run: yarn run build:test
|
||||||
|
|
||||||
- aws-s3/sync:
|
- aws-s3/sync:
|
||||||
from: build
|
from: build
|
||||||
to: "s3://imex-online-test/"
|
to: "s3://imex-online-test/"
|
||||||
arguments: "--exclude '*.map'"
|
|
||||||
- jira/notify
|
- jira/notify
|
||||||
|
|
||||||
admin-app-build:
|
admin-app-build:
|
||||||
@@ -134,6 +152,10 @@ jobs:
|
|||||||
workflows:
|
workflows:
|
||||||
deploy_and_build:
|
deploy_and_build:
|
||||||
jobs:
|
jobs:
|
||||||
|
- api-deploy:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master
|
||||||
- app-build:
|
- app-build:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -118,5 +118,3 @@ logs/oAuthClient-log.log
|
|||||||
.node-persist/**
|
.node-persist/**
|
||||||
|
|
||||||
/*.env.*
|
/*.env.*
|
||||||
.idea/*
|
|
||||||
.idea
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
exports.default = {
|
|
||||||
printWidth: 120,
|
|
||||||
useTabs: false,
|
|
||||||
tabWidth: 2,
|
|
||||||
trailingComma: "es5",
|
|
||||||
semi: true,
|
|
||||||
singleQuote: false,
|
|
||||||
bracketSpacing: true,
|
|
||||||
arrowParens: "always",
|
|
||||||
jsxSingleQuote: false,
|
|
||||||
bracketSameLine: false,
|
|
||||||
endOfLine: "lf",
|
|
||||||
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
|
||||||
importOrderSeparation: true,
|
|
||||||
importOrderSortSpecifiers: true,
|
|
||||||
};
|
|
||||||
11
README.MD
11
README.MD
@@ -1,3 +1,14 @@
|
|||||||
|
Yarn Dependency Management:
|
||||||
|
To force upgrades for some packages:
|
||||||
|
yarn upgrade-interactive --latest
|
||||||
|
|
||||||
|
To Start Hasura CLI:
|
||||||
|
npx hasura console
|
||||||
|
|
||||||
|
Migrating to Staging:
|
||||||
|
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||||
|
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
|
||||||
|
|
||||||
NGROK TEsting:
|
NGROK TEsting:
|
||||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||||
|
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
For filters and sorters, valid types include (`type` key in the schema):
|
|
||||||
- string (default)
|
|
||||||
- number
|
|
||||||
- bool or boolean
|
|
||||||
- date
|
|
||||||
|
|
||||||
## Special Notes
|
|
||||||
- When passing the data to the template server, the property filters and sorters is added to the data object and will reflect the filters and sorters the user has selected
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"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`
|
|
||||||
- `special.referral_sources` - This will reflect the referral sources `bodyshop.md_referral_sources`
|
|
||||||
- `special.class`- This will reflect the class `bodyshop.md_classes`
|
|
||||||
- `special.lost_sale_reasons` - This will reflect the lost sale reasons `bodyshop.md_lost_sale_reasons`
|
|
||||||
- `special.alt_transports` - This will reflect the alternative transports `bodyshop.appt_alt_transport`
|
|
||||||
- `special.payment_types` - This will reflect the payment types `bodyshop.md_payment_types`
|
|
||||||
- `special.payment_payers` - This is a special case with a key value set of [Customer, Insurance]
|
|
||||||
|
|
||||||
### 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 type object must be 'string' or 'number' or 'bool' or 'boolean' or 'date' 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.
|
|
||||||
- *INHERITANCE CAVEAT* If you have a filters file on a parent report that has a child that you do not want the filters inherited from, you must place a blank filters file (valid json so {}) on the child report level. This will than fetch the child filters, which are empty and move along, versus inheriting the parent filters.
|
|
||||||
|
|
||||||
## 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,6 @@ REACT_APP_CLOUDINARY_API_KEY=957865933348715
|
|||||||
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/
|
||||||
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
GENERATE_SOURCEMAP=true
|
|
||||||
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
|
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
|
||||||
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
|
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
|
||||||
REACT_APP_GA_CODE=231103507
|
REACT_APP_GA_CODE=231103507
|
||||||
|
|||||||
3
client/.gitignore
vendored
3
client/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
|
|
||||||
# Sentry Config File
|
|
||||||
.sentryclirc
|
|
||||||
@@ -1,25 +1,25 @@
|
|||||||
// craco.config.js
|
// craco.config.js
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
//const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
// {
|
{
|
||||||
// plugin: SentryWebpackPlugin,
|
plugin: SentryWebpackPlugin,
|
||||||
// options: {
|
options: {
|
||||||
// // sentry-cli configuration
|
// sentry-cli configuration
|
||||||
// authToken:
|
authToken:
|
||||||
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
||||||
// org: "snapt-software",
|
org: "snapt-software",
|
||||||
// project: "imexonline",
|
project: "imexonline",
|
||||||
// release: process.env.REACT_APP_GIT_SHA,
|
release: process.env.REACT_APP_GIT_SHA,
|
||||||
|
|
||||||
// // webpack-specific configuration
|
// webpack-specific configuration
|
||||||
// include: ".",
|
include: ".",
|
||||||
// ignore: ["node_modules", "webpack.config.js"],
|
ignore: ["node_modules", "webpack.config.js"],
|
||||||
// },
|
},
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
plugin: CracoLessPlugin,
|
plugin: CracoLessPlugin,
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
9
client/cypress.config.js
Normal file
9
client/cypress.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:3000",
|
|
||||||
"experimentalStudio": true,
|
|
||||||
"env": {
|
|
||||||
"FIREBASE_USERNAME": "cypress@imex.test",
|
|
||||||
"FIREBASE_PASSWORD": "cypress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
client/cypress/e2e/1-auth/login.cy.js
Normal file
18
client/cypress/e2e/1-auth/login.cy.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context("Actions", () => {
|
||||||
|
beforeEach(() => {});
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
|
||||||
|
it("Attempt a failed login.", () => {
|
||||||
|
cy.visit("http://localhost:3000");
|
||||||
|
cy.get(".ant-btn").contains("Sign In").click();
|
||||||
|
cy.get("#email").type("fake@email.com");
|
||||||
|
cy.get("#password").type("fakepassword");
|
||||||
|
cy.get(".ant-btn").contains("Login").click();
|
||||||
|
cy.should("have.text", "user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Attempt a failed login.", () => {});
|
||||||
|
});
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"id": 8739,
|
|
||||||
"name": "Jane",
|
|
||||||
"email": "jane@example.com"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/// <reference types="Cypress" />
|
|
||||||
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
|
||||||
describe("Renders the General Page", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit("/");
|
|
||||||
});
|
|
||||||
it("Renders Correctly", () => {});
|
|
||||||
it("Has the Slogan", () => {
|
|
||||||
cy.findByText("A whole x22new kind of shop management system.").should(
|
|
||||||
"exist"
|
|
||||||
);
|
|
||||||
/* ==== Generated with Cypress Studio ==== */
|
|
||||||
cy.get(
|
|
||||||
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
|
|
||||||
).click();
|
|
||||||
cy.get("#email").clear();
|
|
||||||
cy.get("#email").type("patrick@imex.dev");
|
|
||||||
cy.get("#password").clear();
|
|
||||||
cy.get("#password").type("patrick123{enter}");
|
|
||||||
cy.get(".ant-form > .ant-btn").click();
|
|
||||||
/* ==== End Cypress Studio ==== */
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
// Welcome to Cypress!
|
|
||||||
//
|
|
||||||
// This spec file contains a variety of sample tests
|
|
||||||
// for a todo list app that are designed to demonstrate
|
|
||||||
// the power of writing tests in Cypress.
|
|
||||||
//
|
|
||||||
// To learn more about how Cypress works and
|
|
||||||
// what makes it such an awesome testing tool,
|
|
||||||
// please read our getting started guide:
|
|
||||||
// https://on.cypress.io/introduction-to-cypress
|
|
||||||
|
|
||||||
describe('example to-do app', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Cypress starts out with a blank slate for each test
|
|
||||||
// so we must tell it to visit our website with the `cy.visit()` command.
|
|
||||||
// Since we want to visit the same URL at the start of all our tests,
|
|
||||||
// we include it in our beforeEach function so that it runs before each test
|
|
||||||
cy.visit('https://example.cypress.io/todo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('displays two todo items by default', () => {
|
|
||||||
// We use the `cy.get()` command to get all elements that match the selector.
|
|
||||||
// Then, we use `should` to assert that there are two matched items,
|
|
||||||
// which are the two default items.
|
|
||||||
cy.get('.todo-list li').should('have.length', 2)
|
|
||||||
|
|
||||||
// We can go even further and check that the default todos each contain
|
|
||||||
// the correct text. We use the `first` and `last` functions
|
|
||||||
// to get just the first and last matched elements individually,
|
|
||||||
// and then perform an assertion with `should`.
|
|
||||||
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
|
|
||||||
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can add new todo items', () => {
|
|
||||||
// We'll store our item text in a variable so we can reuse it
|
|
||||||
const newItem = 'Feed the cat'
|
|
||||||
|
|
||||||
// Let's get the input element and use the `type` command to
|
|
||||||
// input our new list item. After typing the content of our item,
|
|
||||||
// we need to type the enter key as well in order to submit the input.
|
|
||||||
// This input has a data-test attribute so we'll use that to select the
|
|
||||||
// element in accordance with best practices:
|
|
||||||
// https://on.cypress.io/selecting-elements
|
|
||||||
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
|
|
||||||
|
|
||||||
// Now that we've typed our new item, let's check that it actually was added to the list.
|
|
||||||
// Since it's the newest item, it should exist as the last element in the list.
|
|
||||||
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
|
||||||
// Since assertions yield the element that was asserted on,
|
|
||||||
// we can chain both of these assertions together into a single statement.
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 3)
|
|
||||||
.last()
|
|
||||||
.should('have.text', newItem)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can check off an item as completed', () => {
|
|
||||||
// In addition to using the `get` command to get an element by selector,
|
|
||||||
// we can also use the `contains` command to get an element by its contents.
|
|
||||||
// However, this will yield the <label>, which is lowest-level element that contains the text.
|
|
||||||
// In order to check the item, we'll find the <input> element for this <label>
|
|
||||||
// by traversing up the dom to the parent element. From there, we can `find`
|
|
||||||
// the child checkbox <input> element and use the `check` command to check it.
|
|
||||||
cy.contains('Pay electric bill')
|
|
||||||
.parent()
|
|
||||||
.find('input[type=checkbox]')
|
|
||||||
.check()
|
|
||||||
|
|
||||||
// Now that we've checked the button, we can go ahead and make sure
|
|
||||||
// that the list element is now marked as completed.
|
|
||||||
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
|
||||||
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
|
||||||
// Once we get that element, we can assert that it has the completed class.
|
|
||||||
cy.contains('Pay electric bill')
|
|
||||||
.parents('li')
|
|
||||||
.should('have.class', 'completed')
|
|
||||||
})
|
|
||||||
|
|
||||||
context('with a checked task', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// We'll take the command we used above to check off an element
|
|
||||||
// Since we want to perform multiple tests that start with checking
|
|
||||||
// one element, we put it in the beforeEach hook
|
|
||||||
// so that it runs at the start of every test.
|
|
||||||
cy.contains('Pay electric bill')
|
|
||||||
.parent()
|
|
||||||
.find('input[type=checkbox]')
|
|
||||||
.check()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can filter for uncompleted tasks', () => {
|
|
||||||
// We'll click on the "active" button in order to
|
|
||||||
// display only incomplete items
|
|
||||||
cy.contains('Active').click()
|
|
||||||
|
|
||||||
// After filtering, we can assert that there is only the one
|
|
||||||
// incomplete item in the list.
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 1)
|
|
||||||
.first()
|
|
||||||
.should('have.text', 'Walk the dog')
|
|
||||||
|
|
||||||
// For good measure, let's also assert that the task we checked off
|
|
||||||
// does not exist on the page.
|
|
||||||
cy.contains('Pay electric bill').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can filter for completed tasks', () => {
|
|
||||||
// We can perform similar steps as the test above to ensure
|
|
||||||
// that only completed tasks are shown
|
|
||||||
cy.contains('Completed').click()
|
|
||||||
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 1)
|
|
||||||
.first()
|
|
||||||
.should('have.text', 'Pay electric bill')
|
|
||||||
|
|
||||||
cy.contains('Walk the dog').should('not.exist')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can delete all completed tasks', () => {
|
|
||||||
// First, let's click the "Clear completed" button
|
|
||||||
// `contains` is actually serving two purposes here.
|
|
||||||
// First, it's ensuring that the button exists within the dom.
|
|
||||||
// This button only appears when at least one task is checked
|
|
||||||
// so this command is implicitly verifying that it does exist.
|
|
||||||
// Second, it selects the button so we can click it.
|
|
||||||
cy.contains('Clear completed').click()
|
|
||||||
|
|
||||||
// Then we can make sure that there is only one element
|
|
||||||
// in the list and our element does not exist
|
|
||||||
cy.get('.todo-list li')
|
|
||||||
.should('have.length', 1)
|
|
||||||
.should('not.have.text', 'Pay electric bill')
|
|
||||||
|
|
||||||
// Finally, make sure that the clear button no longer exists.
|
|
||||||
cy.contains('Clear completed').should('not.exist')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Actions', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/actions')
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
|
||||||
|
|
||||||
it('.type() - type into a DOM element', () => {
|
|
||||||
// https://on.cypress.io/type
|
|
||||||
cy.get('.action-email')
|
|
||||||
.type('fake@email.com').should('have.value', 'fake@email.com')
|
|
||||||
|
|
||||||
// .type() with special character sequences
|
|
||||||
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
|
|
||||||
.type('{del}{selectall}{backspace}')
|
|
||||||
|
|
||||||
// .type() with key modifiers
|
|
||||||
.type('{alt}{option}') //these are equivalent
|
|
||||||
.type('{ctrl}{control}') //these are equivalent
|
|
||||||
.type('{meta}{command}{cmd}') //these are equivalent
|
|
||||||
.type('{shift}')
|
|
||||||
|
|
||||||
// Delay each keypress by 0.1 sec
|
|
||||||
.type('slow.typing@email.com', { delay: 100 })
|
|
||||||
.should('have.value', 'slow.typing@email.com')
|
|
||||||
|
|
||||||
cy.get('.action-disabled')
|
|
||||||
// Ignore error checking prior to type
|
|
||||||
// like whether the input is visible or disabled
|
|
||||||
.type('disabled error checking', { force: true })
|
|
||||||
.should('have.value', 'disabled error checking')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.focus() - focus on a DOM element', () => {
|
|
||||||
// https://on.cypress.io/focus
|
|
||||||
cy.get('.action-focus').focus()
|
|
||||||
.should('have.class', 'focus')
|
|
||||||
.prev().should('have.attr', 'style', 'color: orange;')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.blur() - blur off a DOM element', () => {
|
|
||||||
// https://on.cypress.io/blur
|
|
||||||
cy.get('.action-blur').type('About to blur').blur()
|
|
||||||
.should('have.class', 'error')
|
|
||||||
.prev().should('have.attr', 'style', 'color: red;')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.clear() - clears an input or textarea element', () => {
|
|
||||||
// https://on.cypress.io/clear
|
|
||||||
cy.get('.action-clear').type('Clear this text')
|
|
||||||
.should('have.value', 'Clear this text')
|
|
||||||
.clear()
|
|
||||||
.should('have.value', '')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.submit() - submit a form', () => {
|
|
||||||
// https://on.cypress.io/submit
|
|
||||||
cy.get('.action-form')
|
|
||||||
.find('[type="text"]').type('HALFOFF')
|
|
||||||
|
|
||||||
cy.get('.action-form').submit()
|
|
||||||
.next().should('contain', 'Your form has been submitted!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.click() - click on a DOM element', () => {
|
|
||||||
// https://on.cypress.io/click
|
|
||||||
cy.get('.action-btn').click()
|
|
||||||
|
|
||||||
// You can click on 9 specific positions of an element:
|
|
||||||
// -----------------------------------
|
|
||||||
// | topLeft top topRight |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | left center right |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | bottomLeft bottom bottomRight |
|
|
||||||
// -----------------------------------
|
|
||||||
|
|
||||||
// clicking in the center of the element is the default
|
|
||||||
cy.get('#action-canvas').click()
|
|
||||||
|
|
||||||
cy.get('#action-canvas').click('topLeft')
|
|
||||||
cy.get('#action-canvas').click('top')
|
|
||||||
cy.get('#action-canvas').click('topRight')
|
|
||||||
cy.get('#action-canvas').click('left')
|
|
||||||
cy.get('#action-canvas').click('right')
|
|
||||||
cy.get('#action-canvas').click('bottomLeft')
|
|
||||||
cy.get('#action-canvas').click('bottom')
|
|
||||||
cy.get('#action-canvas').click('bottomRight')
|
|
||||||
|
|
||||||
// .click() accepts an x and y coordinate
|
|
||||||
// that controls where the click occurs :)
|
|
||||||
|
|
||||||
cy.get('#action-canvas')
|
|
||||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
|
||||||
.click(170, 75)
|
|
||||||
.click(80, 165)
|
|
||||||
.click(100, 185)
|
|
||||||
.click(125, 190)
|
|
||||||
.click(150, 185)
|
|
||||||
.click(170, 165)
|
|
||||||
|
|
||||||
// click multiple elements by passing multiple: true
|
|
||||||
cy.get('.action-labels>.label').click({ multiple: true })
|
|
||||||
|
|
||||||
// Ignore error checking prior to clicking
|
|
||||||
cy.get('.action-opacity>.btn').click({ force: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.dblclick() - double click on a DOM element', () => {
|
|
||||||
// https://on.cypress.io/dblclick
|
|
||||||
|
|
||||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
|
||||||
// that hides the div and shows an input on double click
|
|
||||||
cy.get('.action-div').dblclick().should('not.be.visible')
|
|
||||||
cy.get('.action-input-hidden').should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.rightclick() - right click on a DOM element', () => {
|
|
||||||
// https://on.cypress.io/rightclick
|
|
||||||
|
|
||||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
|
||||||
// that hides the div and shows an input on right click
|
|
||||||
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
|
|
||||||
cy.get('.rightclick-action-input-hidden').should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.check() - check a checkbox or radio element', () => {
|
|
||||||
// https://on.cypress.io/check
|
|
||||||
|
|
||||||
// By default, .check() will check all
|
|
||||||
// matching checkbox or radio elements in succession, one after another
|
|
||||||
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
|
|
||||||
.check().should('be.checked')
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]').not('[disabled]')
|
|
||||||
.check().should('be.checked')
|
|
||||||
|
|
||||||
// .check() accepts a value argument
|
|
||||||
cy.get('.action-radios [type="radio"]')
|
|
||||||
.check('radio1').should('be.checked')
|
|
||||||
|
|
||||||
// .check() accepts an array of values
|
|
||||||
cy.get('.action-multiple-checkboxes [type="checkbox"]')
|
|
||||||
.check(['checkbox1', 'checkbox2']).should('be.checked')
|
|
||||||
|
|
||||||
// Ignore error checking prior to checking
|
|
||||||
cy.get('.action-checkboxes [disabled]')
|
|
||||||
.check({ force: true }).should('be.checked')
|
|
||||||
|
|
||||||
cy.get('.action-radios [type="radio"]')
|
|
||||||
.check('radio3', { force: true }).should('be.checked')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.uncheck() - uncheck a checkbox element', () => {
|
|
||||||
// https://on.cypress.io/uncheck
|
|
||||||
|
|
||||||
// By default, .uncheck() will uncheck all matching
|
|
||||||
// checkbox elements in succession, one after another
|
|
||||||
cy.get('.action-check [type="checkbox"]')
|
|
||||||
.not('[disabled]')
|
|
||||||
.uncheck().should('not.be.checked')
|
|
||||||
|
|
||||||
// .uncheck() accepts a value argument
|
|
||||||
cy.get('.action-check [type="checkbox"]')
|
|
||||||
.check('checkbox1')
|
|
||||||
.uncheck('checkbox1').should('not.be.checked')
|
|
||||||
|
|
||||||
// .uncheck() accepts an array of values
|
|
||||||
cy.get('.action-check [type="checkbox"]')
|
|
||||||
.check(['checkbox1', 'checkbox3'])
|
|
||||||
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
|
|
||||||
|
|
||||||
// Ignore error checking prior to unchecking
|
|
||||||
cy.get('.action-check [disabled]')
|
|
||||||
.uncheck({ force: true }).should('not.be.checked')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.select() - select an option in a <select> element', () => {
|
|
||||||
// https://on.cypress.io/select
|
|
||||||
|
|
||||||
// at first, no option should be selected
|
|
||||||
cy.get('.action-select')
|
|
||||||
.should('have.value', '--Select a fruit--')
|
|
||||||
|
|
||||||
// Select option(s) with matching text content
|
|
||||||
cy.get('.action-select').select('apples')
|
|
||||||
// confirm the apples were selected
|
|
||||||
// note that each value starts with "fr-" in our HTML
|
|
||||||
cy.get('.action-select').should('have.value', 'fr-apples')
|
|
||||||
|
|
||||||
cy.get('.action-select-multiple')
|
|
||||||
.select(['apples', 'oranges', 'bananas'])
|
|
||||||
// when getting multiple values, invoke "val" method first
|
|
||||||
.invoke('val')
|
|
||||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
|
||||||
|
|
||||||
// Select option(s) with matching value
|
|
||||||
cy.get('.action-select').select('fr-bananas')
|
|
||||||
// can attach an assertion right away to the element
|
|
||||||
.should('have.value', 'fr-bananas')
|
|
||||||
|
|
||||||
cy.get('.action-select-multiple')
|
|
||||||
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
|
|
||||||
.invoke('val')
|
|
||||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
|
||||||
|
|
||||||
// assert the selected values include oranges
|
|
||||||
cy.get('.action-select-multiple')
|
|
||||||
.invoke('val').should('include', 'fr-oranges')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.scrollIntoView() - scroll an element into view', () => {
|
|
||||||
// https://on.cypress.io/scrollintoview
|
|
||||||
|
|
||||||
// normally all of these buttons are hidden,
|
|
||||||
// because they're not within
|
|
||||||
// the viewable area of their parent
|
|
||||||
// (we need to scroll to see them)
|
|
||||||
cy.get('#scroll-horizontal button')
|
|
||||||
.should('not.be.visible')
|
|
||||||
|
|
||||||
// scroll the button into view, as if the user had scrolled
|
|
||||||
cy.get('#scroll-horizontal button').scrollIntoView()
|
|
||||||
.should('be.visible')
|
|
||||||
|
|
||||||
cy.get('#scroll-vertical button')
|
|
||||||
.should('not.be.visible')
|
|
||||||
|
|
||||||
// Cypress handles the scroll direction needed
|
|
||||||
cy.get('#scroll-vertical button').scrollIntoView()
|
|
||||||
.should('be.visible')
|
|
||||||
|
|
||||||
cy.get('#scroll-both button')
|
|
||||||
.should('not.be.visible')
|
|
||||||
|
|
||||||
// Cypress knows to scroll to the right and down
|
|
||||||
cy.get('#scroll-both button').scrollIntoView()
|
|
||||||
.should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.trigger() - trigger an event on a DOM element', () => {
|
|
||||||
// https://on.cypress.io/trigger
|
|
||||||
|
|
||||||
// To interact with a range input (slider)
|
|
||||||
// we need to set its value & trigger the
|
|
||||||
// event to signal it changed
|
|
||||||
|
|
||||||
// Here, we invoke jQuery's val() method to set
|
|
||||||
// the value and trigger the 'change' event
|
|
||||||
cy.get('.trigger-input-range')
|
|
||||||
.invoke('val', 25)
|
|
||||||
.trigger('change')
|
|
||||||
.get('input[type=range]').siblings('p')
|
|
||||||
.should('have.text', '25')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.scrollTo() - scroll the window or element to a position', () => {
|
|
||||||
// https://on.cypress.io/scrollto
|
|
||||||
|
|
||||||
// You can scroll to 9 specific positions of an element:
|
|
||||||
// -----------------------------------
|
|
||||||
// | topLeft top topRight |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | left center right |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | bottomLeft bottom bottomRight |
|
|
||||||
// -----------------------------------
|
|
||||||
|
|
||||||
// if you chain .scrollTo() off of cy, we will
|
|
||||||
// scroll the entire window
|
|
||||||
cy.scrollTo('bottom')
|
|
||||||
|
|
||||||
cy.get('#scrollable-horizontal').scrollTo('right')
|
|
||||||
|
|
||||||
// or you can scroll to a specific coordinate:
|
|
||||||
// (x axis, y axis) in pixels
|
|
||||||
cy.get('#scrollable-vertical').scrollTo(250, 250)
|
|
||||||
|
|
||||||
// or you can scroll to a specific percentage
|
|
||||||
// of the (width, height) of the element
|
|
||||||
cy.get('#scrollable-both').scrollTo('75%', '25%')
|
|
||||||
|
|
||||||
// control the easing of the scroll (default is 'swing')
|
|
||||||
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
|
|
||||||
|
|
||||||
// control the duration of the scroll (in ms)
|
|
||||||
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Aliasing', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/aliasing')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.as() - alias a DOM element for later use', () => {
|
|
||||||
// https://on.cypress.io/as
|
|
||||||
|
|
||||||
// Alias a DOM element for use later
|
|
||||||
// We don't have to traverse to the element
|
|
||||||
// later in our code, we reference it with @
|
|
||||||
|
|
||||||
cy.get('.as-table').find('tbody>tr')
|
|
||||||
.first().find('td').first()
|
|
||||||
.find('button').as('firstBtn')
|
|
||||||
|
|
||||||
// when we reference the alias, we place an
|
|
||||||
// @ in front of its name
|
|
||||||
cy.get('@firstBtn').click()
|
|
||||||
|
|
||||||
cy.get('@firstBtn')
|
|
||||||
.should('have.class', 'btn-success')
|
|
||||||
.and('contain', 'Changed')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.as() - alias a route for later use', () => {
|
|
||||||
// Alias the route to wait for its response
|
|
||||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.network-btn').click()
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
cy.wait('@getComment').its('response.statusCode').should('eq', 200)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Assertions', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/assertions')
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Implicit Assertions', () => {
|
|
||||||
it('.should() - make an assertion about the current subject', () => {
|
|
||||||
// https://on.cypress.io/should
|
|
||||||
cy.get('.assertion-table')
|
|
||||||
.find('tbody tr:last')
|
|
||||||
.should('have.class', 'success')
|
|
||||||
.find('td')
|
|
||||||
.first()
|
|
||||||
// checking the text of the <td> element in various ways
|
|
||||||
.should('have.text', 'Column content')
|
|
||||||
.should('contain', 'Column content')
|
|
||||||
.should('have.html', 'Column content')
|
|
||||||
// chai-jquery uses "is()" to check if element matches selector
|
|
||||||
.should('match', 'td')
|
|
||||||
// to match text content against a regular expression
|
|
||||||
// first need to invoke jQuery method text()
|
|
||||||
// and then match using regular expression
|
|
||||||
.invoke('text')
|
|
||||||
.should('match', /column content/i)
|
|
||||||
|
|
||||||
// a better way to check element's text content against a regular expression
|
|
||||||
// is to use "cy.contains"
|
|
||||||
// https://on.cypress.io/contains
|
|
||||||
cy.get('.assertion-table')
|
|
||||||
.find('tbody tr:last')
|
|
||||||
// finds first <td> element with text content matching regular expression
|
|
||||||
.contains('td', /column content/i)
|
|
||||||
.should('be.visible')
|
|
||||||
|
|
||||||
// for more information about asserting element's text
|
|
||||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.and() - chain multiple assertions together', () => {
|
|
||||||
// https://on.cypress.io/and
|
|
||||||
cy.get('.assertions-link')
|
|
||||||
.should('have.class', 'active')
|
|
||||||
.and('have.attr', 'href')
|
|
||||||
.and('include', 'cypress.io')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Explicit Assertions', () => {
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
it('expect - make an assertion about a specified subject', () => {
|
|
||||||
// We can use Chai's BDD style assertions
|
|
||||||
expect(true).to.be.true
|
|
||||||
const o = { foo: 'bar' }
|
|
||||||
|
|
||||||
expect(o).to.equal(o)
|
|
||||||
expect(o).to.deep.equal({ foo: 'bar' })
|
|
||||||
// matching text using regular expression
|
|
||||||
expect('FooBar').to.match(/bar$/i)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('pass your own callback function to should()', () => {
|
|
||||||
// Pass a function to should that can have any number
|
|
||||||
// of explicit assertions within it.
|
|
||||||
// The ".should(cb)" function will be retried
|
|
||||||
// automatically until it passes all your explicit assertions or times out.
|
|
||||||
cy.get('.assertions-p')
|
|
||||||
.find('p')
|
|
||||||
.should(($p) => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
// return an array of texts from all of the p's
|
|
||||||
// @ts-ignore TS6133 unused variable
|
|
||||||
const texts = $p.map((i, el) => Cypress.$(el).text())
|
|
||||||
|
|
||||||
// jquery map returns jquery object
|
|
||||||
// and .get() convert this to simple array
|
|
||||||
const paragraphs = texts.get()
|
|
||||||
|
|
||||||
// array should have length of 3
|
|
||||||
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
|
|
||||||
|
|
||||||
// use second argument to expect(...) to provide clear
|
|
||||||
// message with each assertion
|
|
||||||
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
|
|
||||||
'Some text from first p',
|
|
||||||
'More text from second p',
|
|
||||||
'And even more text from third p',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('finds element by class name regex', () => {
|
|
||||||
cy.get('.docs-header')
|
|
||||||
.find('div')
|
|
||||||
// .should(cb) callback function will be retried
|
|
||||||
.should(($div) => {
|
|
||||||
expect($div).to.have.length(1)
|
|
||||||
|
|
||||||
const className = $div[0].className
|
|
||||||
|
|
||||||
expect(className).to.match(/heading-/)
|
|
||||||
})
|
|
||||||
// .then(cb) callback is not retried,
|
|
||||||
// it either passes or fails
|
|
||||||
.then(($div) => {
|
|
||||||
expect($div, 'text content').to.have.text('Introduction')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('can throw any error', () => {
|
|
||||||
cy.get('.docs-header')
|
|
||||||
.find('div')
|
|
||||||
.should(($div) => {
|
|
||||||
if ($div.length !== 1) {
|
|
||||||
// you can throw your own errors
|
|
||||||
throw new Error('Did not find 1 element')
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = $div[0].className
|
|
||||||
|
|
||||||
if (!className.match(/heading-/)) {
|
|
||||||
throw new Error(`Could not find class "heading-" in ${className}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches unknown text between two elements', () => {
|
|
||||||
/**
|
|
||||||
* Text from the first element.
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let text
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes passed text,
|
|
||||||
* useful before comparing text with spaces and different capitalization.
|
|
||||||
* @param {string} s Text to normalize
|
|
||||||
*/
|
|
||||||
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
|
|
||||||
|
|
||||||
cy.get('.two-elements')
|
|
||||||
.find('.first')
|
|
||||||
.then(($first) => {
|
|
||||||
// save text from the first element
|
|
||||||
text = normalizeText($first.text())
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('.two-elements')
|
|
||||||
.find('.second')
|
|
||||||
.should(($div) => {
|
|
||||||
// we can massage text before comparing
|
|
||||||
const secondText = normalizeText($div.text())
|
|
||||||
|
|
||||||
expect(secondText, 'second text').to.equal(text)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('assert - assert shape of an object', () => {
|
|
||||||
const person = {
|
|
||||||
name: 'Joe',
|
|
||||||
age: 20,
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.isObject(person, 'value is object')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('retries the should callback until assertions pass', () => {
|
|
||||||
cy.get('#random-number')
|
|
||||||
.should(($div) => {
|
|
||||||
const n = parseFloat($div.text())
|
|
||||||
|
|
||||||
expect(n).to.be.gte(1).and.be.lte(10)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Connectors', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/connectors')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.each() - iterate over an array of elements', () => {
|
|
||||||
// https://on.cypress.io/each
|
|
||||||
cy.get('.connectors-each-ul>li')
|
|
||||||
.each(($el, index, $list) => {
|
|
||||||
console.log($el, index, $list)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.its() - get properties on the current subject', () => {
|
|
||||||
// https://on.cypress.io/its
|
|
||||||
cy.get('.connectors-its-ul>li')
|
|
||||||
// calls the 'length' property yielding that value
|
|
||||||
.its('length')
|
|
||||||
.should('be.gt', 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.invoke() - invoke a function on the current subject', () => {
|
|
||||||
// our div is hidden in our script.js
|
|
||||||
// $('.connectors-div').hide()
|
|
||||||
|
|
||||||
// https://on.cypress.io/invoke
|
|
||||||
cy.get('.connectors-div').should('be.hidden')
|
|
||||||
// call the jquery method 'show' on the 'div.container'
|
|
||||||
.invoke('show')
|
|
||||||
.should('be.visible')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.spread() - spread an array as individual args to callback function', () => {
|
|
||||||
// https://on.cypress.io/spread
|
|
||||||
const arr = ['foo', 'bar', 'baz']
|
|
||||||
|
|
||||||
cy.wrap(arr).spread((foo, bar, baz) => {
|
|
||||||
expect(foo).to.eq('foo')
|
|
||||||
expect(bar).to.eq('bar')
|
|
||||||
expect(baz).to.eq('baz')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('.then()', () => {
|
|
||||||
it('invokes a callback function with the current subject', () => {
|
|
||||||
// https://on.cypress.io/then
|
|
||||||
cy.get('.connectors-list > li')
|
|
||||||
.then(($lis) => {
|
|
||||||
expect($lis, '3 items').to.have.length(3)
|
|
||||||
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
|
|
||||||
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
|
|
||||||
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('yields the returned value to the next command', () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1)
|
|
||||||
|
|
||||||
return 2
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('yields the original subject without return', () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1)
|
|
||||||
// note that nothing is returned from this callback
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the original unchanged value 1
|
|
||||||
expect(num).to.equal(1)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('yields the value yielded by the last Cypress command inside', () => {
|
|
||||||
cy.wrap(1)
|
|
||||||
.then((num) => {
|
|
||||||
expect(num).to.equal(1)
|
|
||||||
// note how we run a Cypress command
|
|
||||||
// the result yielded by this Cypress command
|
|
||||||
// will be passed to the second ".then"
|
|
||||||
cy.wrap(2)
|
|
||||||
})
|
|
||||||
.then((num) => {
|
|
||||||
// this callback receives the value yielded by "cy.wrap(2)"
|
|
||||||
expect(num).to.equal(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Cookies', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
Cypress.Cookies.debug(true)
|
|
||||||
|
|
||||||
cy.visit('https://example.cypress.io/commands/cookies')
|
|
||||||
|
|
||||||
// clear cookies again after visiting to remove
|
|
||||||
// any 3rd party cookies picked up such as cloudflare
|
|
||||||
cy.clearCookies()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.getCookie() - get a browser cookie', () => {
|
|
||||||
// https://on.cypress.io/getcookie
|
|
||||||
cy.get('#getCookie .set-a-cookie').click()
|
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
|
||||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.getCookies() - get browser cookies', () => {
|
|
||||||
// https://on.cypress.io/getcookies
|
|
||||||
cy.getCookies().should('be.empty')
|
|
||||||
|
|
||||||
cy.get('#getCookies .set-a-cookie').click()
|
|
||||||
|
|
||||||
// cy.getCookies() yields an array of cookies
|
|
||||||
cy.getCookies().should('have.length', 1).should((cookies) => {
|
|
||||||
// each cookie has these properties
|
|
||||||
expect(cookies[0]).to.have.property('name', 'token')
|
|
||||||
expect(cookies[0]).to.have.property('value', '123ABC')
|
|
||||||
expect(cookies[0]).to.have.property('httpOnly', false)
|
|
||||||
expect(cookies[0]).to.have.property('secure', false)
|
|
||||||
expect(cookies[0]).to.have.property('domain')
|
|
||||||
expect(cookies[0]).to.have.property('path')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.setCookie() - set a browser cookie', () => {
|
|
||||||
// https://on.cypress.io/setcookie
|
|
||||||
cy.getCookies().should('be.empty')
|
|
||||||
|
|
||||||
cy.setCookie('foo', 'bar')
|
|
||||||
|
|
||||||
// cy.getCookie() yields a cookie object
|
|
||||||
cy.getCookie('foo').should('have.property', 'value', 'bar')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.clearCookie() - clear a browser cookie', () => {
|
|
||||||
// https://on.cypress.io/clearcookie
|
|
||||||
cy.getCookie('token').should('be.null')
|
|
||||||
|
|
||||||
cy.get('#clearCookie .set-a-cookie').click()
|
|
||||||
|
|
||||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
|
||||||
cy.clearCookie('token').should('be.null')
|
|
||||||
|
|
||||||
cy.getCookie('token').should('be.null')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.clearCookies() - clear browser cookies', () => {
|
|
||||||
// https://on.cypress.io/clearcookies
|
|
||||||
cy.getCookies().should('be.empty')
|
|
||||||
|
|
||||||
cy.get('#clearCookies .set-a-cookie').click()
|
|
||||||
|
|
||||||
cy.getCookies().should('have.length', 1)
|
|
||||||
|
|
||||||
// cy.clearCookies() yields null
|
|
||||||
cy.clearCookies()
|
|
||||||
|
|
||||||
cy.getCookies().should('be.empty')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Cypress.Commands', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
|
|
||||||
it('.add() - create a custom command', () => {
|
|
||||||
Cypress.Commands.add('console', {
|
|
||||||
prevSubject: true,
|
|
||||||
}, (subject, method) => {
|
|
||||||
// the previous subject is automatically received
|
|
||||||
// and the commands arguments are shifted
|
|
||||||
|
|
||||||
// allow us to change the console method used
|
|
||||||
method = method || 'log'
|
|
||||||
|
|
||||||
// log the subject to the console
|
|
||||||
// @ts-ignore TS7017
|
|
||||||
console[method]('The subject is', subject)
|
|
||||||
|
|
||||||
// whatever we return becomes the new subject
|
|
||||||
// we don't want to change the subject so
|
|
||||||
// we return whatever was passed in
|
|
||||||
return subject
|
|
||||||
})
|
|
||||||
|
|
||||||
// @ts-ignore TS2339
|
|
||||||
cy.get('button').console('info').then(($button) => {
|
|
||||||
// subject is still $button
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.Cookies', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://on.cypress.io/cookies
|
|
||||||
it('.debug() - enable or disable debugging', () => {
|
|
||||||
Cypress.Cookies.debug(true)
|
|
||||||
|
|
||||||
// Cypress will now log in the console when
|
|
||||||
// cookies are set or cleared
|
|
||||||
cy.setCookie('fakeCookie', '123ABC')
|
|
||||||
cy.clearCookie('fakeCookie')
|
|
||||||
cy.setCookie('fakeCookie', '123ABC')
|
|
||||||
cy.clearCookie('fakeCookie')
|
|
||||||
cy.setCookie('fakeCookie', '123ABC')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.preserveOnce() - preserve cookies by key', () => {
|
|
||||||
// normally cookies are reset after each test
|
|
||||||
cy.getCookie('fakeCookie').should('not.be.ok')
|
|
||||||
|
|
||||||
// preserving a cookie will not clear it when
|
|
||||||
// the next test starts
|
|
||||||
cy.setCookie('lastCookie', '789XYZ')
|
|
||||||
Cypress.Cookies.preserveOnce('lastCookie')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.defaults() - set defaults for all cookies', () => {
|
|
||||||
// now any cookie with the name 'session_id' will
|
|
||||||
// not be cleared before each new test runs
|
|
||||||
Cypress.Cookies.defaults({
|
|
||||||
preserve: 'session_id',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.arch', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Get CPU architecture name of underlying OS', () => {
|
|
||||||
// https://on.cypress.io/arch
|
|
||||||
expect(Cypress.arch).to.exist
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.config()', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Get and set configuration options', () => {
|
|
||||||
// https://on.cypress.io/config
|
|
||||||
let myConfig = Cypress.config()
|
|
||||||
|
|
||||||
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
|
|
||||||
expect(myConfig).to.have.property('baseUrl', null)
|
|
||||||
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
|
|
||||||
expect(myConfig).to.have.property('requestTimeout', 5000)
|
|
||||||
expect(myConfig).to.have.property('responseTimeout', 30000)
|
|
||||||
expect(myConfig).to.have.property('viewportHeight', 660)
|
|
||||||
expect(myConfig).to.have.property('viewportWidth', 1000)
|
|
||||||
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
|
|
||||||
expect(myConfig).to.have.property('waitForAnimations', true)
|
|
||||||
|
|
||||||
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
|
|
||||||
|
|
||||||
// this will change the config for the rest of your tests!
|
|
||||||
Cypress.config('pageLoadTimeout', 20000)
|
|
||||||
|
|
||||||
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
|
|
||||||
|
|
||||||
Cypress.config('pageLoadTimeout', 60000)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.dom', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
// https://on.cypress.io/dom
|
|
||||||
it('.isHidden() - determine if a DOM element is hidden', () => {
|
|
||||||
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
|
|
||||||
let visibleP = Cypress.$('.dom-p p.visible').get(0)
|
|
||||||
|
|
||||||
// our first paragraph has css class 'hidden'
|
|
||||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
|
|
||||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.env()', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
// We can set environment variables for highly dynamic values
|
|
||||||
|
|
||||||
// https://on.cypress.io/environment-variables
|
|
||||||
it('Get environment variables', () => {
|
|
||||||
// https://on.cypress.io/env
|
|
||||||
// set multiple environment variables
|
|
||||||
Cypress.env({
|
|
||||||
host: 'veronica.dev.local',
|
|
||||||
api_server: 'http://localhost:8888/v1/',
|
|
||||||
})
|
|
||||||
|
|
||||||
// get environment variable
|
|
||||||
expect(Cypress.env('host')).to.eq('veronica.dev.local')
|
|
||||||
|
|
||||||
// set environment variable
|
|
||||||
Cypress.env('api_server', 'http://localhost:8888/v2/')
|
|
||||||
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
|
|
||||||
|
|
||||||
// get all environment variable
|
|
||||||
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
|
|
||||||
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.log', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Control what is printed to the Command Log', () => {
|
|
||||||
// https://on.cypress.io/cypress-log
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.platform', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Get underlying OS name', () => {
|
|
||||||
// https://on.cypress.io/platform
|
|
||||||
expect(Cypress.platform).to.be.exist
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.version', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Get current version of Cypress being run', () => {
|
|
||||||
// https://on.cypress.io/version
|
|
||||||
expect(Cypress.version).to.be.exist
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.spec', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/cypress-api')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Get current spec information', () => {
|
|
||||||
// https://on.cypress.io/spec
|
|
||||||
// wrap the object so we can inspect it easily by clicking in the command log
|
|
||||||
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
/// JSON fixture file can be loaded directly using
|
|
||||||
// the built-in JavaScript bundler
|
|
||||||
// @ts-ignore
|
|
||||||
const requiredExample = require('../../fixtures/example')
|
|
||||||
|
|
||||||
context('Files', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/files')
|
|
||||||
})
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// load example.json fixture file and store
|
|
||||||
// in the test context object
|
|
||||||
cy.fixture('example.json').as('example')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.fixture() - load a fixture', () => {
|
|
||||||
// https://on.cypress.io/fixture
|
|
||||||
|
|
||||||
// Instead of writing a response inline you can
|
|
||||||
// use a fixture file's content.
|
|
||||||
|
|
||||||
// when application makes an Ajax request matching "GET **/comments/*"
|
|
||||||
// Cypress will intercept it and reply with the object in `example.json` fixture
|
|
||||||
cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment')
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.fixture-btn').click()
|
|
||||||
|
|
||||||
cy.wait('@getComment').its('response.body')
|
|
||||||
.should('have.property', 'name')
|
|
||||||
.and('include', 'Using fixtures to represent data')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.fixture() or require - load a fixture', function () {
|
|
||||||
// we are inside the "function () { ... }"
|
|
||||||
// callback and can use test context object "this"
|
|
||||||
// "this.example" was loaded in "beforeEach" function callback
|
|
||||||
expect(this.example, 'fixture in the test context')
|
|
||||||
.to.deep.equal(requiredExample)
|
|
||||||
|
|
||||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
|
||||||
cy.wrap(this.example)
|
|
||||||
.should('deep.equal', requiredExample)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.readFile() - read file contents', () => {
|
|
||||||
// https://on.cypress.io/readfile
|
|
||||||
|
|
||||||
// You can read a file and yield its contents
|
|
||||||
// The filePath is relative to your project's root.
|
|
||||||
cy.readFile('cypress.json').then((json) => {
|
|
||||||
expect(json).to.be.an('object')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.writeFile() - write to a file', () => {
|
|
||||||
// https://on.cypress.io/writefile
|
|
||||||
|
|
||||||
// You can write to a file
|
|
||||||
|
|
||||||
// Use a response from a request to automatically
|
|
||||||
// generate a fixture file for use later
|
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
|
||||||
.then((response) => {
|
|
||||||
cy.writeFile('cypress/fixtures/users.json', response.body)
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.fixture('users').should((users) => {
|
|
||||||
expect(users[0].name).to.exist
|
|
||||||
})
|
|
||||||
|
|
||||||
// JavaScript arrays and objects are stringified
|
|
||||||
// and formatted into text.
|
|
||||||
cy.writeFile('cypress/fixtures/profile.json', {
|
|
||||||
id: 8739,
|
|
||||||
name: 'Jane',
|
|
||||||
email: 'jane@example.com',
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.fixture('profile').should((profile) => {
|
|
||||||
expect(profile.name).to.eq('Jane')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Local Storage', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/local-storage')
|
|
||||||
})
|
|
||||||
// Although local storage is automatically cleared
|
|
||||||
// in between tests to maintain a clean state
|
|
||||||
// sometimes we need to clear the local storage manually
|
|
||||||
|
|
||||||
it('cy.clearLocalStorage() - clear all data in local storage', () => {
|
|
||||||
// https://on.cypress.io/clearlocalstorage
|
|
||||||
cy.get('.ls-btn').click().should(() => {
|
|
||||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
|
||||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
|
||||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
|
||||||
})
|
|
||||||
|
|
||||||
// clearLocalStorage() yields the localStorage object
|
|
||||||
cy.clearLocalStorage().should((ls) => {
|
|
||||||
expect(ls.getItem('prop1')).to.be.null
|
|
||||||
expect(ls.getItem('prop2')).to.be.null
|
|
||||||
expect(ls.getItem('prop3')).to.be.null
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('.ls-btn').click().should(() => {
|
|
||||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
|
||||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
|
||||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clear key matching string in Local Storage
|
|
||||||
cy.clearLocalStorage('prop1').should((ls) => {
|
|
||||||
expect(ls.getItem('prop1')).to.be.null
|
|
||||||
expect(ls.getItem('prop2')).to.eq('blue')
|
|
||||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
|
||||||
})
|
|
||||||
|
|
||||||
cy.get('.ls-btn').click().should(() => {
|
|
||||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
|
||||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
|
||||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clear keys matching regex in Local Storage
|
|
||||||
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
|
||||||
expect(ls.getItem('prop1')).to.be.null
|
|
||||||
expect(ls.getItem('prop2')).to.be.null
|
|
||||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Location', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/location')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.hash() - get the current URL hash', () => {
|
|
||||||
// https://on.cypress.io/hash
|
|
||||||
cy.hash().should('be.empty')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.location() - get window.location', () => {
|
|
||||||
// https://on.cypress.io/location
|
|
||||||
cy.location().should((location) => {
|
|
||||||
expect(location.hash).to.be.empty
|
|
||||||
expect(location.href).to.eq('https://example.cypress.io/commands/location')
|
|
||||||
expect(location.host).to.eq('example.cypress.io')
|
|
||||||
expect(location.hostname).to.eq('example.cypress.io')
|
|
||||||
expect(location.origin).to.eq('https://example.cypress.io')
|
|
||||||
expect(location.pathname).to.eq('/commands/location')
|
|
||||||
expect(location.port).to.eq('')
|
|
||||||
expect(location.protocol).to.eq('https:')
|
|
||||||
expect(location.search).to.be.empty
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.url() - get the current URL', () => {
|
|
||||||
// https://on.cypress.io/url
|
|
||||||
cy.url().should('eq', 'https://example.cypress.io/commands/location')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Misc', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/misc')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.end() - end the command chain', () => {
|
|
||||||
// https://on.cypress.io/end
|
|
||||||
|
|
||||||
// cy.end is useful when you want to end a chain of commands
|
|
||||||
// and force Cypress to re-query from the root element
|
|
||||||
cy.get('.misc-table').within(() => {
|
|
||||||
// ends the current chain and yields null
|
|
||||||
cy.contains('Cheryl').click().end()
|
|
||||||
|
|
||||||
// queries the entire table again
|
|
||||||
cy.contains('Charles').click()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.exec() - execute a system command', () => {
|
|
||||||
// execute a system command.
|
|
||||||
// so you can take actions necessary for
|
|
||||||
// your test outside the scope of Cypress.
|
|
||||||
// https://on.cypress.io/exec
|
|
||||||
|
|
||||||
// we can use Cypress.platform string to
|
|
||||||
// select appropriate command
|
|
||||||
// https://on.cypress/io/platform
|
|
||||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
|
|
||||||
|
|
||||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
|
||||||
// https://github.com/cypress-io/cypress/issues/5169
|
|
||||||
// so skip some of the tests by passing flag "--env circle=true"
|
|
||||||
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
|
|
||||||
|
|
||||||
if (isCircleOnWindows) {
|
|
||||||
cy.log('Skipping test on CircleCI')
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// cy.exec problem on Shippable CI
|
|
||||||
// https://github.com/cypress-io/cypress/issues/6718
|
|
||||||
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
|
|
||||||
|
|
||||||
if (isShippable) {
|
|
||||||
cy.log('Skipping test on ShippableCI')
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.exec('echo Jane Lane')
|
|
||||||
.its('stdout').should('contain', 'Jane Lane')
|
|
||||||
|
|
||||||
if (Cypress.platform === 'win32') {
|
|
||||||
cy.exec('print cypress.json')
|
|
||||||
.its('stderr').should('be.empty')
|
|
||||||
} else {
|
|
||||||
cy.exec('cat cypress.json')
|
|
||||||
.its('stderr').should('be.empty')
|
|
||||||
|
|
||||||
cy.exec('pwd')
|
|
||||||
.its('code').should('eq', 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.focused() - get the DOM element that has focus', () => {
|
|
||||||
// https://on.cypress.io/focused
|
|
||||||
cy.get('.misc-form').find('#name').click()
|
|
||||||
cy.focused().should('have.id', 'name')
|
|
||||||
|
|
||||||
cy.get('.misc-form').find('#description').click()
|
|
||||||
cy.focused().should('have.id', 'description')
|
|
||||||
})
|
|
||||||
|
|
||||||
context('Cypress.Screenshot', function () {
|
|
||||||
it('cy.screenshot() - take a screenshot', () => {
|
|
||||||
// https://on.cypress.io/screenshot
|
|
||||||
cy.screenshot('my-image')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
|
|
||||||
Cypress.Screenshot.defaults({
|
|
||||||
blackout: ['.foo'],
|
|
||||||
capture: 'viewport',
|
|
||||||
clip: { x: 0, y: 0, width: 200, height: 200 },
|
|
||||||
scale: false,
|
|
||||||
disableTimersAndAnimations: true,
|
|
||||||
screenshotOnRunFailure: true,
|
|
||||||
onBeforeScreenshot () { },
|
|
||||||
onAfterScreenshot () { },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.wrap() - wrap an object', () => {
|
|
||||||
// https://on.cypress.io/wrap
|
|
||||||
cy.wrap({ foo: 'bar' })
|
|
||||||
.should('have.property', 'foo')
|
|
||||||
.and('include', 'bar')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Navigation', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io')
|
|
||||||
cy.get('.navbar-nav').contains('Commands').click()
|
|
||||||
cy.get('.dropdown-menu').contains('Navigation').click()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.go() - go back or forward in the browser\'s history', () => {
|
|
||||||
// https://on.cypress.io/go
|
|
||||||
|
|
||||||
cy.location('pathname').should('include', 'navigation')
|
|
||||||
|
|
||||||
cy.go('back')
|
|
||||||
cy.location('pathname').should('not.include', 'navigation')
|
|
||||||
|
|
||||||
cy.go('forward')
|
|
||||||
cy.location('pathname').should('include', 'navigation')
|
|
||||||
|
|
||||||
// clicking back
|
|
||||||
cy.go(-1)
|
|
||||||
cy.location('pathname').should('not.include', 'navigation')
|
|
||||||
|
|
||||||
// clicking forward
|
|
||||||
cy.go(1)
|
|
||||||
cy.location('pathname').should('include', 'navigation')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.reload() - reload the page', () => {
|
|
||||||
// https://on.cypress.io/reload
|
|
||||||
cy.reload()
|
|
||||||
|
|
||||||
// reload the page without using the cache
|
|
||||||
cy.reload(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.visit() - visit a remote url', () => {
|
|
||||||
// https://on.cypress.io/visit
|
|
||||||
|
|
||||||
// Visit any sub-domain of your current domain
|
|
||||||
|
|
||||||
// Pass options to the visit
|
|
||||||
cy.visit('https://example.cypress.io/commands/navigation', {
|
|
||||||
timeout: 50000, // increase total time for the visit to resolve
|
|
||||||
onBeforeLoad (contentWindow) {
|
|
||||||
// contentWindow is the remote page's window object
|
|
||||||
expect(typeof contentWindow === 'object').to.be.true
|
|
||||||
},
|
|
||||||
onLoad (contentWindow) {
|
|
||||||
// contentWindow is the remote page's window object
|
|
||||||
expect(typeof contentWindow === 'object').to.be.true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Network Requests', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/network-requests')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Manage HTTP requests in your app
|
|
||||||
|
|
||||||
it('cy.request() - make an XHR request', () => {
|
|
||||||
// https://on.cypress.io/request
|
|
||||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
|
||||||
.should((response) => {
|
|
||||||
expect(response.status).to.eq(200)
|
|
||||||
// the server sometimes gets an extra comment posted from another machine
|
|
||||||
// which gets returned as 1 extra object
|
|
||||||
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
|
|
||||||
expect(response).to.have.property('headers')
|
|
||||||
expect(response).to.have.property('duration')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() - verify response using BDD syntax', () => {
|
|
||||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
|
||||||
.then((response) => {
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
expect(response).property('status').to.equal(200)
|
|
||||||
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
|
|
||||||
expect(response).to.include.keys('headers', 'duration')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() with query parameters', () => {
|
|
||||||
// will execute request
|
|
||||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
|
||||||
cy.request({
|
|
||||||
url: 'https://jsonplaceholder.cypress.io/comments',
|
|
||||||
qs: {
|
|
||||||
postId: 1,
|
|
||||||
id: 3,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.its('body')
|
|
||||||
.should('be.an', 'array')
|
|
||||||
.and('have.length', 1)
|
|
||||||
.its('0') // yields first element of the array
|
|
||||||
.should('contain', {
|
|
||||||
postId: 1,
|
|
||||||
id: 3,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() - pass result to the second request', () => {
|
|
||||||
// first, let's find out the userId of the first user we have
|
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
|
||||||
.its('body') // yields the response object
|
|
||||||
.its('0') // yields the first element of the returned list
|
|
||||||
// the above two commands its('body').its('0')
|
|
||||||
// can be written as its('body.0')
|
|
||||||
// if you do not care about TypeScript checks
|
|
||||||
.then((user) => {
|
|
||||||
expect(user).property('id').to.be.a('number')
|
|
||||||
// make a new post on behalf of the user
|
|
||||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
|
||||||
userId: user.id,
|
|
||||||
title: 'Cypress Test Runner',
|
|
||||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// note that the value here is the returned value of the 2nd request
|
|
||||||
// which is the new post object
|
|
||||||
.then((response) => {
|
|
||||||
expect(response).property('status').to.equal(201) // new entity created
|
|
||||||
expect(response).property('body').to.contain({
|
|
||||||
title: 'Cypress Test Runner',
|
|
||||||
})
|
|
||||||
|
|
||||||
// we don't know the exact post id - only that it will be > 100
|
|
||||||
// since JSONPlaceholder has built-in 100 posts
|
|
||||||
expect(response.body).property('id').to.be.a('number')
|
|
||||||
.and.to.be.gt(100)
|
|
||||||
|
|
||||||
// we don't know the user id here - since it was in above closure
|
|
||||||
// so in this test just confirm that the property is there
|
|
||||||
expect(response.body).property('userId').to.be.a('number')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.request() - save response in the shared test context', () => {
|
|
||||||
// https://on.cypress.io/variables-and-aliases
|
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
|
||||||
.its('body').its('0') // yields the first element of the returned list
|
|
||||||
.as('user') // saves the object in the test context
|
|
||||||
.then(function () {
|
|
||||||
// NOTE 👀
|
|
||||||
// By the time this callback runs the "as('user')" command
|
|
||||||
// has saved the user object in the test context.
|
|
||||||
// To access the test context we need to use
|
|
||||||
// the "function () { ... }" callback form,
|
|
||||||
// otherwise "this" points at a wrong or undefined object!
|
|
||||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
|
||||||
userId: this.user.id,
|
|
||||||
title: 'Cypress Test Runner',
|
|
||||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
|
||||||
})
|
|
||||||
.its('body').as('post') // save the new post from the response
|
|
||||||
})
|
|
||||||
.then(function () {
|
|
||||||
// When this callback runs, both "cy.request" API commands have finished
|
|
||||||
// and the test context has "user" and "post" objects set.
|
|
||||||
// Let's verify them.
|
|
||||||
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.intercept() - route responses to matching requests', () => {
|
|
||||||
// https://on.cypress.io/intercept
|
|
||||||
|
|
||||||
let message = 'whoa, this comment does not exist'
|
|
||||||
|
|
||||||
// Listen to GET to comments/1
|
|
||||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.network-btn').click()
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
|
||||||
|
|
||||||
// Listen to POST to comments
|
|
||||||
cy.intercept('POST', '**/comments').as('postComment')
|
|
||||||
|
|
||||||
// we have code that posts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.network-post').click()
|
|
||||||
cy.wait('@postComment').should(({ request, response }) => {
|
|
||||||
expect(request.body).to.include('email')
|
|
||||||
expect(request.headers).to.have.property('content-type')
|
|
||||||
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stub a response to PUT comments/ ****
|
|
||||||
cy.intercept({
|
|
||||||
method: 'PUT',
|
|
||||||
url: '**/comments/*',
|
|
||||||
}, {
|
|
||||||
statusCode: 404,
|
|
||||||
body: { error: message },
|
|
||||||
headers: { 'access-control-allow-origin': '*' },
|
|
||||||
delayMs: 500,
|
|
||||||
}).as('putComment')
|
|
||||||
|
|
||||||
// we have code that puts a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.network-put').click()
|
|
||||||
|
|
||||||
cy.wait('@putComment')
|
|
||||||
|
|
||||||
// our 404 statusCode logic in scripts.js executed
|
|
||||||
cy.get('.network-put-comment').should('contain', message)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Querying', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/querying')
|
|
||||||
})
|
|
||||||
|
|
||||||
// The most commonly used query is 'cy.get()', you can
|
|
||||||
// think of this like the '$' in jQuery
|
|
||||||
|
|
||||||
it('cy.get() - query DOM elements', () => {
|
|
||||||
// https://on.cypress.io/get
|
|
||||||
|
|
||||||
cy.get('#query-btn').should('contain', 'Button')
|
|
||||||
|
|
||||||
cy.get('.query-btn').should('contain', 'Button')
|
|
||||||
|
|
||||||
cy.get('#querying .well>button:first').should('contain', 'Button')
|
|
||||||
// ↲
|
|
||||||
// Use CSS selectors just like jQuery
|
|
||||||
|
|
||||||
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
|
|
||||||
|
|
||||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
|
||||||
// by invoking `.attr()` method
|
|
||||||
cy.get('[data-test-id="test-example"]')
|
|
||||||
.invoke('attr', 'data-test-id')
|
|
||||||
.should('equal', 'test-example')
|
|
||||||
|
|
||||||
// or you can get element's CSS property
|
|
||||||
cy.get('[data-test-id="test-example"]')
|
|
||||||
.invoke('css', 'position')
|
|
||||||
.should('equal', 'static')
|
|
||||||
|
|
||||||
// or use assertions directly during 'cy.get()'
|
|
||||||
// https://on.cypress.io/assertions
|
|
||||||
cy.get('[data-test-id="test-example"]')
|
|
||||||
.should('have.attr', 'data-test-id', 'test-example')
|
|
||||||
.and('have.css', 'position', 'static')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.contains() - query DOM elements with matching content', () => {
|
|
||||||
// https://on.cypress.io/contains
|
|
||||||
cy.get('.query-list')
|
|
||||||
.contains('bananas')
|
|
||||||
.should('have.class', 'third')
|
|
||||||
|
|
||||||
// we can pass a regexp to `.contains()`
|
|
||||||
cy.get('.query-list')
|
|
||||||
.contains(/^b\w+/)
|
|
||||||
.should('have.class', 'third')
|
|
||||||
|
|
||||||
cy.get('.query-list')
|
|
||||||
.contains('apples')
|
|
||||||
.should('have.class', 'first')
|
|
||||||
|
|
||||||
// passing a selector to contains will
|
|
||||||
// yield the selector containing the text
|
|
||||||
cy.get('#querying')
|
|
||||||
.contains('ul', 'oranges')
|
|
||||||
.should('have.class', 'query-list')
|
|
||||||
|
|
||||||
cy.get('.query-button')
|
|
||||||
.contains('Save Form')
|
|
||||||
.should('have.class', 'btn')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.within() - query DOM elements within a specific element', () => {
|
|
||||||
// https://on.cypress.io/within
|
|
||||||
cy.get('.query-form').within(() => {
|
|
||||||
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
|
|
||||||
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.root() - query the root DOM element', () => {
|
|
||||||
// https://on.cypress.io/root
|
|
||||||
|
|
||||||
// By default, root is the document
|
|
||||||
cy.root().should('match', 'html')
|
|
||||||
|
|
||||||
cy.get('.query-ul').within(() => {
|
|
||||||
// In this within, the root is now the ul DOM element
|
|
||||||
cy.root().should('have.class', 'query-ul')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('best practices - selecting elements', () => {
|
|
||||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
|
||||||
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
|
|
||||||
// Worst - too generic, no context
|
|
||||||
cy.get('button').click()
|
|
||||||
|
|
||||||
// Bad. Coupled to styling. Highly subject to change.
|
|
||||||
cy.get('.btn.btn-large').click()
|
|
||||||
|
|
||||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
|
||||||
cy.get('[name=submission]').click()
|
|
||||||
|
|
||||||
// Better. But still coupled to styling or JS event listeners.
|
|
||||||
cy.get('#main').click()
|
|
||||||
|
|
||||||
// Slightly better. Uses an ID but also ensures the element
|
|
||||||
// has an ARIA role attribute
|
|
||||||
cy.get('#main[role=button]').click()
|
|
||||||
|
|
||||||
// Much better. But still coupled to text content that may change.
|
|
||||||
cy.contains('Submit').click()
|
|
||||||
|
|
||||||
// Best. Insulated from all changes.
|
|
||||||
cy.get('[data-cy=submit]').click()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// remove no check once Cypress.sinon is typed
|
|
||||||
// https://github.com/cypress-io/cypress/issues/6720
|
|
||||||
|
|
||||||
context('Spies, Stubs, and Clock', () => {
|
|
||||||
it('cy.spy() - wrap a method in a spy', () => {
|
|
||||||
// https://on.cypress.io/spy
|
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
foo () {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const spy = cy.spy(obj, 'foo').as('anyArgs')
|
|
||||||
|
|
||||||
obj.foo()
|
|
||||||
|
|
||||||
expect(spy).to.be.called
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.spy() retries until assertions pass', () => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
/**
|
|
||||||
* Prints the argument passed
|
|
||||||
* @param x {any}
|
|
||||||
*/
|
|
||||||
foo (x) {
|
|
||||||
console.log('obj.foo called with', x)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.spy(obj, 'foo').as('foo')
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
obj.foo('first')
|
|
||||||
}, 500)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
obj.foo('second')
|
|
||||||
}, 2500)
|
|
||||||
|
|
||||||
cy.get('@foo').should('have.been.calledTwice')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.stub() - create a stub and/or replace a function with stub', () => {
|
|
||||||
// https://on.cypress.io/stub
|
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
|
||||||
|
|
||||||
const obj = {
|
|
||||||
/**
|
|
||||||
* prints both arguments to the console
|
|
||||||
* @param a {string}
|
|
||||||
* @param b {string}
|
|
||||||
*/
|
|
||||||
foo (a, b) {
|
|
||||||
console.log('a', a, 'b', b)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const stub = cy.stub(obj, 'foo').as('foo')
|
|
||||||
|
|
||||||
obj.foo('foo', 'bar')
|
|
||||||
|
|
||||||
expect(stub).to.be.called
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.clock() - control time in the browser', () => {
|
|
||||||
// https://on.cypress.io/clock
|
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
|
||||||
// no matter what local timezone the browser is running in
|
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
|
||||||
|
|
||||||
cy.clock(now)
|
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
|
||||||
cy.get('#clock-div').click()
|
|
||||||
.should('have.text', '1489449600')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.tick() - move time in the browser', () => {
|
|
||||||
// https://on.cypress.io/tick
|
|
||||||
|
|
||||||
// create the date in UTC so its always the same
|
|
||||||
// no matter what local timezone the browser is running in
|
|
||||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
|
||||||
|
|
||||||
cy.clock(now)
|
|
||||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
|
||||||
cy.get('#tick-div').click()
|
|
||||||
.should('have.text', '1489449600')
|
|
||||||
|
|
||||||
cy.tick(10000) // 10 seconds passed
|
|
||||||
cy.get('#tick-div').click()
|
|
||||||
.should('have.text', '1489449610')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.stub() matches depending on arguments', () => {
|
|
||||||
// see all possible matchers at
|
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||||
const greeter = {
|
|
||||||
/**
|
|
||||||
* Greets a person
|
|
||||||
* @param {string} name
|
|
||||||
*/
|
|
||||||
greet (name) {
|
|
||||||
return `Hello, ${name}!`
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.stub(greeter, 'greet')
|
|
||||||
.callThrough() // if you want non-matched calls to call the real method
|
|
||||||
.withArgs(Cypress.sinon.match.string).returns('Hi')
|
|
||||||
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
|
|
||||||
|
|
||||||
expect(greeter.greet('World')).to.equal('Hi')
|
|
||||||
// @ts-ignore
|
|
||||||
expect(() => greeter.greet(42)).to.throw('Invalid name')
|
|
||||||
expect(greeter.greet).to.have.been.calledTwice
|
|
||||||
|
|
||||||
// non-matched calls goes the actual method
|
|
||||||
// @ts-ignore
|
|
||||||
expect(greeter.greet()).to.equal('Hello, undefined!')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('matches call arguments using Sinon matchers', () => {
|
|
||||||
// see all possible matchers at
|
|
||||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||||
const calculator = {
|
|
||||||
/**
|
|
||||||
* returns the sum of two arguments
|
|
||||||
* @param a {number}
|
|
||||||
* @param b {number}
|
|
||||||
*/
|
|
||||||
add (a, b) {
|
|
||||||
return a + b
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const spy = cy.spy(calculator, 'add').as('add')
|
|
||||||
|
|
||||||
expect(calculator.add(2, 3)).to.equal(5)
|
|
||||||
|
|
||||||
// if we want to assert the exact values used during the call
|
|
||||||
expect(spy).to.be.calledWith(2, 3)
|
|
||||||
|
|
||||||
// let's confirm "add" method was called with two numbers
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
|
|
||||||
|
|
||||||
// alternatively, provide the value to match
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
|
|
||||||
|
|
||||||
// match any value
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
|
|
||||||
|
|
||||||
// match any value from a list
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given number is event
|
|
||||||
* @param {number} x
|
|
||||||
*/
|
|
||||||
const isEven = (x) => x % 2 === 0
|
|
||||||
|
|
||||||
// expect the value to pass a custom predicate function
|
|
||||||
// the second argument to "sinon.match(predicate, message)" is
|
|
||||||
// shown if the predicate does not pass and assertion fails
|
|
||||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that checks if a given number is larger than the limit
|
|
||||||
* @param {number} limit
|
|
||||||
* @returns {(x: number) => boolean}
|
|
||||||
*/
|
|
||||||
const isGreaterThan = (limit) => (x) => x > limit
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function that checks if a given number is less than the limit
|
|
||||||
* @param {number} limit
|
|
||||||
* @returns {(x: number) => boolean}
|
|
||||||
*/
|
|
||||||
const isLessThan = (limit) => (x) => x < limit
|
|
||||||
|
|
||||||
// you can combine several matchers using "and", "or"
|
|
||||||
expect(spy).to.be.calledWith(
|
|
||||||
Cypress.sinon.match.number,
|
|
||||||
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(spy).to.be.calledWith(
|
|
||||||
Cypress.sinon.match.number,
|
|
||||||
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// matchers can be used from BDD assertions
|
|
||||||
cy.get('@add').should('have.been.calledWith',
|
|
||||||
Cypress.sinon.match.number, Cypress.sinon.match(3))
|
|
||||||
|
|
||||||
// you can alias matchers for shorter test code
|
|
||||||
const { match: M } = Cypress.sinon
|
|
||||||
|
|
||||||
cy.get('@add').should('have.been.calledWith', M.number, M(3))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Traversal', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/traversal')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.children() - get child DOM elements', () => {
|
|
||||||
// https://on.cypress.io/children
|
|
||||||
cy.get('.traversal-breadcrumb')
|
|
||||||
.children('.active')
|
|
||||||
.should('contain', 'Data')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.closest() - get closest ancestor DOM element', () => {
|
|
||||||
// https://on.cypress.io/closest
|
|
||||||
cy.get('.traversal-badge')
|
|
||||||
.closest('ul')
|
|
||||||
.should('have.class', 'list-group')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.eq() - get a DOM element at a specific index', () => {
|
|
||||||
// https://on.cypress.io/eq
|
|
||||||
cy.get('.traversal-list>li')
|
|
||||||
.eq(1).should('contain', 'siamese')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.filter() - get DOM elements that match the selector', () => {
|
|
||||||
// https://on.cypress.io/filter
|
|
||||||
cy.get('.traversal-nav>li')
|
|
||||||
.filter('.active').should('contain', 'About')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.find() - get descendant DOM elements of the selector', () => {
|
|
||||||
// https://on.cypress.io/find
|
|
||||||
cy.get('.traversal-pagination')
|
|
||||||
.find('li').find('a')
|
|
||||||
.should('have.length', 7)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.first() - get first DOM element', () => {
|
|
||||||
// https://on.cypress.io/first
|
|
||||||
cy.get('.traversal-table td')
|
|
||||||
.first().should('contain', '1')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.last() - get last DOM element', () => {
|
|
||||||
// https://on.cypress.io/last
|
|
||||||
cy.get('.traversal-buttons .btn')
|
|
||||||
.last().should('contain', 'Submit')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.next() - get next sibling DOM element', () => {
|
|
||||||
// https://on.cypress.io/next
|
|
||||||
cy.get('.traversal-ul')
|
|
||||||
.contains('apples').next().should('contain', 'oranges')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.nextAll() - get all next sibling DOM elements', () => {
|
|
||||||
// https://on.cypress.io/nextall
|
|
||||||
cy.get('.traversal-next-all')
|
|
||||||
.contains('oranges')
|
|
||||||
.nextAll().should('have.length', 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.nextUntil() - get next sibling DOM elements until next el', () => {
|
|
||||||
// https://on.cypress.io/nextuntil
|
|
||||||
cy.get('#veggies')
|
|
||||||
.nextUntil('#nuts').should('have.length', 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.not() - remove DOM elements from set of DOM elements', () => {
|
|
||||||
// https://on.cypress.io/not
|
|
||||||
cy.get('.traversal-disabled .btn')
|
|
||||||
.not('[disabled]').should('not.contain', 'Disabled')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.parent() - get parent DOM element from DOM elements', () => {
|
|
||||||
// https://on.cypress.io/parent
|
|
||||||
cy.get('.traversal-mark')
|
|
||||||
.parent().should('contain', 'Morbi leo risus')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.parents() - get parent DOM elements from DOM elements', () => {
|
|
||||||
// https://on.cypress.io/parents
|
|
||||||
cy.get('.traversal-cite')
|
|
||||||
.parents().should('match', 'blockquote')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
|
|
||||||
// https://on.cypress.io/parentsuntil
|
|
||||||
cy.get('.clothes-nav')
|
|
||||||
.find('.active')
|
|
||||||
.parentsUntil('.clothes-nav')
|
|
||||||
.should('have.length', 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.prev() - get previous sibling DOM element', () => {
|
|
||||||
// https://on.cypress.io/prev
|
|
||||||
cy.get('.birds').find('.active')
|
|
||||||
.prev().should('contain', 'Lorikeets')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.prevAll() - get all previous sibling DOM elements', () => {
|
|
||||||
// https://on.cypress.io/prevall
|
|
||||||
cy.get('.fruits-list').find('.third')
|
|
||||||
.prevAll().should('have.length', 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
|
|
||||||
// https://on.cypress.io/prevuntil
|
|
||||||
cy.get('.foods-list').find('#nuts')
|
|
||||||
.prevUntil('#veggies').should('have.length', 3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('.siblings() - get all sibling DOM elements', () => {
|
|
||||||
// https://on.cypress.io/siblings
|
|
||||||
cy.get('.traversal-pills .active')
|
|
||||||
.siblings().should('have.length', 2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Utilities', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/utilities')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress._ - call a lodash method', () => {
|
|
||||||
// https://on.cypress.io/_
|
|
||||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
|
||||||
.then((response) => {
|
|
||||||
let ids = Cypress._.chain(response.body).map('id').take(3).value()
|
|
||||||
|
|
||||||
expect(ids).to.deep.eq([1, 2, 3])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.$ - call a jQuery method', () => {
|
|
||||||
// https://on.cypress.io/$
|
|
||||||
let $li = Cypress.$('.utility-jquery li:first')
|
|
||||||
|
|
||||||
cy.wrap($li)
|
|
||||||
.should('not.have.class', 'active')
|
|
||||||
.click()
|
|
||||||
.should('have.class', 'active')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
|
|
||||||
// https://on.cypress.io/blob
|
|
||||||
cy.get('.utility-blob').then(($div) => {
|
|
||||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
|
||||||
// get the dataUrl string for the javascript-logo
|
|
||||||
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
|
|
||||||
.then((dataUrl) => {
|
|
||||||
// create an <img> element and set its src to the dataUrl
|
|
||||||
let img = Cypress.$('<img />', { src: dataUrl })
|
|
||||||
|
|
||||||
// need to explicitly return cy here since we are initially returning
|
|
||||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
|
||||||
// append the image
|
|
||||||
$div.append(img)
|
|
||||||
|
|
||||||
cy.get('.utility-blob img').click()
|
|
||||||
.should('have.attr', 'src', dataUrl)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.minimatch - test out glob patterns against strings', () => {
|
|
||||||
// https://on.cypress.io/minimatch
|
|
||||||
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
|
|
||||||
matchBase: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(matching, 'matching wildcard').to.be.true
|
|
||||||
|
|
||||||
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
|
|
||||||
matchBase: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(matching, 'comments').to.be.false
|
|
||||||
|
|
||||||
// ** matches against all downstream path segments
|
|
||||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
|
|
||||||
matchBase: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(matching, 'comments').to.be.true
|
|
||||||
|
|
||||||
// whereas * matches only the next path segment
|
|
||||||
|
|
||||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
|
|
||||||
matchBase: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(matching, 'comments').to.be.false
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Cypress.Promise - instantiate a bluebird promise', () => {
|
|
||||||
// https://on.cypress.io/promise
|
|
||||||
let waited = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Bluebird<string>
|
|
||||||
*/
|
|
||||||
function waitOneSecond () {
|
|
||||||
// return a promise that resolves after 1 second
|
|
||||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
|
||||||
return new Cypress.Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
// set waited to true
|
|
||||||
waited = true
|
|
||||||
|
|
||||||
// resolve with 'foo' string
|
|
||||||
resolve('foo')
|
|
||||||
}, 1000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
cy.then(() => {
|
|
||||||
// return a promise to cy.then() that
|
|
||||||
// is awaited until it resolves
|
|
||||||
// @ts-ignore TS7006
|
|
||||||
return waitOneSecond().then((str) => {
|
|
||||||
expect(str).to.eq('foo')
|
|
||||||
expect(waited).to.be.true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Viewport', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/viewport')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.viewport() - set the viewport size and dimension', () => {
|
|
||||||
// https://on.cypress.io/viewport
|
|
||||||
|
|
||||||
cy.get('#navbar').should('be.visible')
|
|
||||||
cy.viewport(320, 480)
|
|
||||||
|
|
||||||
// the navbar should have collapse since our screen is smaller
|
|
||||||
cy.get('#navbar').should('not.be.visible')
|
|
||||||
cy.get('.navbar-toggle').should('be.visible').click()
|
|
||||||
cy.get('.nav').find('a').should('be.visible')
|
|
||||||
|
|
||||||
// lets see what our app looks like on a super large screen
|
|
||||||
cy.viewport(2999, 2999)
|
|
||||||
|
|
||||||
// cy.viewport() accepts a set of preset sizes
|
|
||||||
// to easily set the screen to a device's width and height
|
|
||||||
|
|
||||||
// We added a cy.wait() between each viewport change so you can see
|
|
||||||
// the change otherwise it is a little too fast to see :)
|
|
||||||
|
|
||||||
cy.viewport('macbook-15')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('macbook-13')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('macbook-11')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('ipad-2')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('ipad-mini')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('iphone-6+')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('iphone-6')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('iphone-5')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('iphone-4')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('iphone-3')
|
|
||||||
cy.wait(200)
|
|
||||||
|
|
||||||
// cy.viewport() accepts an orientation for all presets
|
|
||||||
// the default orientation is 'portrait'
|
|
||||||
cy.viewport('ipad-2', 'portrait')
|
|
||||||
cy.wait(200)
|
|
||||||
cy.viewport('iphone-4', 'landscape')
|
|
||||||
cy.wait(200)
|
|
||||||
|
|
||||||
// The viewport will be reset back to the default dimensions
|
|
||||||
// in between tests (the default can be set in cypress.json)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Waiting', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/waiting')
|
|
||||||
})
|
|
||||||
// BE CAREFUL of adding unnecessary wait times.
|
|
||||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
|
||||||
|
|
||||||
// https://on.cypress.io/wait
|
|
||||||
it('cy.wait() - wait for a specific amount of time', () => {
|
|
||||||
cy.get('.wait-input1').type('Wait 1000ms after typing')
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.get('.wait-input2').type('Wait 1000ms after typing')
|
|
||||||
cy.wait(1000)
|
|
||||||
cy.get('.wait-input3').type('Wait 1000ms after typing')
|
|
||||||
cy.wait(1000)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.wait() - wait for a specific route', () => {
|
|
||||||
// Listen to GET to comments/1
|
|
||||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
|
||||||
|
|
||||||
// we have code that gets a comment when
|
|
||||||
// the button is clicked in scripts.js
|
|
||||||
cy.get('.network-btn').click()
|
|
||||||
|
|
||||||
// wait for GET comments/1
|
|
||||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context('Window', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit('https://example.cypress.io/commands/window')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.window() - get the global window object', () => {
|
|
||||||
// https://on.cypress.io/window
|
|
||||||
cy.window().should('have.property', 'top')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.document() - get the document object', () => {
|
|
||||||
// https://on.cypress.io/document
|
|
||||||
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cy.title() - get the title', () => {
|
|
||||||
// https://on.cypress.io/title
|
|
||||||
cy.title().should('include', 'Kitchen Sink')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
// ***********************************************************
|
|
||||||
// This example plugins/index.js can be used to load plugins
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off loading
|
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/plugins-guide
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||||
// the project's config changing)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
}
|
|
||||||
@@ -23,5 +23,3 @@
|
|||||||
//
|
//
|
||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
import "@testing-library/cypress/add-commands";
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example support/index.js is processed and
|
// This example support/e2e.js is processed and
|
||||||
// loaded automatically before your test files.
|
// loaded automatically before your test files.
|
||||||
//
|
//
|
||||||
// This is a great place to put global configuration and
|
// This is a great place to put global configuration and
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"baseUrl": "../node_modules",
|
|
||||||
"types": ["cypress"]
|
|
||||||
},
|
|
||||||
"include": ["**/*.*"]
|
|
||||||
}
|
|
||||||
24741
client/package-lock.json
generated
24741
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,73 +4,70 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.9",
|
"@apollo/client": "^3.6.9",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
"@asseinfo/react-kanban": "^2.2.0",
|
||||||
"@craco/craco": "^7.0.0",
|
"@craco/craco": "^6.4.5",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.4.2",
|
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@sentry/cli": "^2.27.0",
|
"@sentry/react": "^7.7.0",
|
||||||
"@sentry/react": "^7.99.0",
|
"@sentry/tracing": "^7.7.0",
|
||||||
"@sentry/tracing": "^7.40.0",
|
"@splitsoftware/splitio-react": "^1.6.0",
|
||||||
"@splitsoftware/splitio-react": "^1.8.1",
|
"@stripe/react-stripe-js": "^1.9.0",
|
||||||
|
"@stripe/stripe-js": "^1.32.0",
|
||||||
"@tanem/react-nprogress": "^5.0.8",
|
"@tanem/react-nprogress": "^5.0.8",
|
||||||
"antd": "^4.24.8",
|
"antd": "^4.22.3",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"axios": "^0.27.2",
|
||||||
"axios": "^1.3.4",
|
"craco-less": "^1.20.0",
|
||||||
"craco-less": "^2.0.0",
|
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"enquire-js": "^0.2.1",
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^9.17.1",
|
"firebase": "^9.9.1",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.5.0",
|
||||||
"i18next": "^22.4.10",
|
"i18next": "^21.8.14",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^6.1.4",
|
||||||
"jsoneditor": "^9.9.0",
|
"jsoneditor": "^9.9.0",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.10.21",
|
"libphonenumber-js": "^1.10.9",
|
||||||
"logrocket": "^3.0.1",
|
"logrocket": "^3.0.1",
|
||||||
"markerjs2": "^2.28.1",
|
"markerjs2": "^2.22.0",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"moment-timezone": "^0.5.41",
|
"moment-timezone": "^0.5.34",
|
||||||
"normalize-url": "^8.0.0",
|
"normalize-url": "^7.0.3",
|
||||||
"phone": "^3.1.35",
|
"phone": "^3.1.23",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.1",
|
||||||
"rc-queue-anim": "^2.0.0",
|
"rc-queue-anim": "^2.0.0",
|
||||||
"rc-scroll-anim": "^2.7.6",
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-big-calendar": "^1.6.8",
|
"react-big-calendar": "^1.5.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-drag-listview": "^0.2.1",
|
"react-drag-listview": "^0.2.1",
|
||||||
"react-grid-gallery": "^1.0.0",
|
"react-grid-gallery": "^0.5.5",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-i18next": "^12.2.0",
|
"react-i18next": "^11.18.1",
|
||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.4.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-number-format": "^4.9.3",
|
||||||
"react-intersection-observer": "^9.4.3",
|
"react-redux": "^7.2.8",
|
||||||
"react-number-format": "^5.1.3",
|
|
||||||
"react-redux": "^8.0.5",
|
|
||||||
"react-resizable": "^3.0.4",
|
"react-resizable": "^3.0.4",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^4.0.3",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-sublime-video": "^0.2.5",
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.3",
|
||||||
"recharts": "^2.4.3",
|
"recharts": "^2.1.12",
|
||||||
"redux": "^4.2.1",
|
"redux": "^4.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.2.2",
|
"redux-saga": "^1.1.3",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^4.1.7",
|
"reselect": "^4.1.6",
|
||||||
"sass": "^1.58.3",
|
"sass": "^1.54.0",
|
||||||
"socket.io-client": "^4.6.1",
|
"socket.io-client": "^4.5.1",
|
||||||
"styled-components": "^5.3.6",
|
"styled-components": "^5.3.5",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"workbox-background-sync": "^6.5.3",
|
"workbox-background-sync": "^6.5.3",
|
||||||
@@ -84,20 +81,17 @@
|
|||||||
"workbox-range-requests": "^6.5.3",
|
"workbox-range-requests": "^6.5.3",
|
||||||
"workbox-routing": "^6.5.3",
|
"workbox-routing": "^6.5.3",
|
||||||
"workbox-strategies": "^6.5.3",
|
"workbox-strategies": "^6.5.3",
|
||||||
"workbox-streams": "^6.5.3",
|
"workbox-streams": "^6.5.3"
|
||||||
"yauzl": "^2.10.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build && npm run sentry:sourcemaps",
|
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||||
"build:test": "env-cmd -f .env.test npm run build",
|
"build:test": "env-cmd -f .env.test yarn run build",
|
||||||
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
"build-deploy:test": "yarn 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",
|
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||||
"test": "cypress open",
|
"test": "cypress open",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject"
|
||||||
"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": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -122,9 +116,8 @@
|
|||||||
"react-error-overlay": "6.0.9"
|
"react-error-overlay": "6.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/webpack-plugin": "^1.20.0",
|
"@sentry/webpack-plugin": "^1.19.0",
|
||||||
"@testing-library/cypress": "^8.0.3",
|
"cypress": "^12.2.0",
|
||||||
"cypress": "^10.3.1",
|
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "6.0.11",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
|
||||||
|
|
||||||
moment.locale("en-US");
|
moment.locale("en-US");
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ export const factory = SplitSdk({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function AppContainer() {
|
export default function AppContainer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,5 +42,3 @@ function AppContainer() {
|
|||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Sentry.withProfiler(AppContainer);
|
|
||||||
|
|||||||
@@ -16,34 +16,48 @@ import TechPageContainer from "../pages/tech/tech.page.container";
|
|||||||
import { setOnline } from "../redux/application/application.actions";
|
import { setOnline } from "../redux/application/application.actions";
|
||||||
import { selectOnline } from "../redux/application/application.selectors";
|
import { selectOnline } from "../redux/application/application.selectors";
|
||||||
import { checkUserSession } from "../redux/user/user.actions";
|
import { checkUserSession } from "../redux/user/user.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../redux/user/user.selectors";
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../redux/user/user.selectors";
|
||||||
import PrivateRoute from "../utils/private-route";
|
import PrivateRoute from "../utils/private-route";
|
||||||
import "./App.styles.scss";
|
import "./App.styles.scss";
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
const ResetPassword = lazy(() =>
|
||||||
|
import("../pages/reset-password/reset-password.component")
|
||||||
|
);
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||||
|
|
||||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||||
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
|
const MobilePaymentContainer = lazy(() =>
|
||||||
|
import("../pages/mobile-payment/mobile-payment.container")
|
||||||
|
);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
online: selectOnline,
|
online: selectOnline,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession()),
|
checkUserSession: () => dispatch(checkUserSession()),
|
||||||
setOnline: (isOnline) => dispatch(setOnline(isOnline))
|
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function App({ bodyshop, checkUserSession, currentUser, online, setOnline }) {
|
export function App({
|
||||||
|
bodyshop,
|
||||||
|
checkUserSession,
|
||||||
|
currentUser,
|
||||||
|
online,
|
||||||
|
setOnline,
|
||||||
|
}) {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
setOnline(false);
|
setOnline(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkUserSession();
|
checkUserSession();
|
||||||
}, [checkUserSession, setOnline]);
|
}, [checkUserSession, setOnline]);
|
||||||
|
|
||||||
@@ -59,7 +73,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
window.addEventListener("online", function (e) {
|
window.addEventListener("online", function (e) {
|
||||||
setOnline(true);
|
setOnline(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser.authorized && bodyshop) {
|
if (currentUser.authorized && bodyshop) {
|
||||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||||
@@ -113,16 +126,32 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
<Route exact path="/disclaimer" component={DisclaimerPage} />
|
<Route exact path="/disclaimer" component={DisclaimerPage} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Route exact path="/mp/:paymentIs" component={MobilePaymentContainer} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/mp/:paymentIs"
|
||||||
|
component={MobilePaymentContainer}
|
||||||
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} path="/manage" component={ManagePage} />
|
<PrivateRoute
|
||||||
|
isAuthorized={currentUser.authorized}
|
||||||
|
path="/manage"
|
||||||
|
component={ManagePage}
|
||||||
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} path="/tech" component={TechPageContainer} />
|
<PrivateRoute
|
||||||
|
isAuthorized={currentUser.authorized}
|
||||||
|
path="/tech"
|
||||||
|
component={TechPageContainer}
|
||||||
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} path="/edit" component={DocumentEditorContainer} />
|
<PrivateRoute
|
||||||
|
isAuthorized={currentUser.authorized}
|
||||||
|
path="/edit"
|
||||||
|
component={DocumentEditorContainer}
|
||||||
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|||||||
@@ -143,28 +143,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//Update row highlighting on production board.
|
//Update row highlighting on production board.
|
||||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
.ant-table-tbody > tr.ant-table-row:hover > td {
|
||||||
background: #e7f3ff !important;
|
background: #eaeaea !important;
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-tbody > tr.ant-table-row-selected > td {
|
|
||||||
background: #e6f7ff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-line-manual {
|
|
||||||
color: tomato;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.ant-table-column-sort {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
|
||||||
background-color: #f4f4f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rowWithColor > td {
|
|
||||||
background-color: var(--bgColor) !important;
|
|
||||||
}
|
}
|
||||||
96
client/src/components/_test/paymentMethod.jsx
Normal file
96
client/src/components/_test/paymentMethod.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
PaymentRequestButtonElement,
|
||||||
|
useStripe,
|
||||||
|
} from "@stripe/react-stripe-js";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||||
|
});
|
||||||
|
|
||||||
|
function Test({ bodyshop, setEmailOptions }) {
|
||||||
|
const stripe = useStripe();
|
||||||
|
|
||||||
|
const [paymentRequest, setPaymentRequest] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (stripe) {
|
||||||
|
const pr = stripe.paymentRequest({
|
||||||
|
country: "CA",
|
||||||
|
displayItems: [{ label: "Deductible", amount: 1099 }],
|
||||||
|
currency: "cad",
|
||||||
|
total: {
|
||||||
|
label: "Demo total",
|
||||||
|
amount: 1099,
|
||||||
|
},
|
||||||
|
requestPayerName: true,
|
||||||
|
requestPayerEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the availability of the Payment Request API.
|
||||||
|
pr.canMakePayment().then((result) => {
|
||||||
|
if (result) {
|
||||||
|
setPaymentRequest(pr);
|
||||||
|
} else {
|
||||||
|
// var details = {
|
||||||
|
// total: { label: "", amount: { currency: "CAD", value: "0.00" } },
|
||||||
|
// };
|
||||||
|
new PaymentRequest(
|
||||||
|
[{ supportedMethods: ["basic-card"] }],
|
||||||
|
{}
|
||||||
|
// details
|
||||||
|
).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [stripe]);
|
||||||
|
|
||||||
|
if (paymentRequest) {
|
||||||
|
return (
|
||||||
|
<div style={{ height: "300px" }}>
|
||||||
|
<PaymentRequestButtonElement options={{ paymentRequest }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEmailOptions({
|
||||||
|
messageOptions: {
|
||||||
|
to: ["patrickwf@gmail.com"],
|
||||||
|
replyTo: bodyshop.email,
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
name: TemplateList().parts_order.key,
|
||||||
|
variables: {
|
||||||
|
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
send email
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Log an ImEX Event.
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"status": 24201299,
|
|
||||||
"custid": 19607899,
|
|
||||||
"paymentid": 24201299,
|
|
||||||
"response": "A",
|
|
||||||
"authcode": "498680",
|
|
||||||
"declinereason": "Approved",
|
|
||||||
"fee": 0,
|
|
||||||
"invoice": "",
|
|
||||||
"account": "john",
|
|
||||||
"amount": 1000,
|
|
||||||
"amountincludesfee": false,
|
|
||||||
"total": 1000,
|
|
||||||
"paymenttype": "C",
|
|
||||||
"methodhint": "VI ***1111",
|
|
||||||
"cardbrand": "Visa",
|
|
||||||
"cardnumdisplay": "***1111",
|
|
||||||
"receiptelements": {
|
|
||||||
"authcode": "498680",
|
|
||||||
"cust_srv_ph_num": "1-555-555-5555",
|
|
||||||
"rcpt_pg_ftr_txt": "Thank You\nPlease Come Again",
|
|
||||||
"rcpt_currency": "USD",
|
|
||||||
"responsecode": "A",
|
|
||||||
"rcpt_pay_mthd": "Visa",
|
|
||||||
"transid": "C00 915799",
|
|
||||||
"merch_disp_nm": "CP Devel Test",
|
|
||||||
"rcpt_input_mthd": "Keyed",
|
|
||||||
"rcpt_pg_hdr_txt": "Welcome!",
|
|
||||||
"rcpt_tran_time": "Thursday February 23 2023, 11:25:36 pm +08",
|
|
||||||
"rcpt_trans_type": "Normal Transaction (Sale)",
|
|
||||||
"message": "Approved",
|
|
||||||
"rcpt_dba_addr": "1234 Storefront Ave\nSome City, UT 84111",
|
|
||||||
"avsdata": "N",
|
|
||||||
"receiptrequirements": "S",
|
|
||||||
"rcpt_cardnum": "************1111",
|
|
||||||
"cv2result": "M",
|
|
||||||
"rfnd_policy_txt": "<b>No Refunds</b>\nStore Credit Only",
|
|
||||||
"labels": {
|
|
||||||
"tranref": "REF#",
|
|
||||||
"tid": "TID",
|
|
||||||
"validationcode": "ValCode",
|
|
||||||
"emvapplicationid": "AID",
|
|
||||||
"emvatc": "ATC",
|
|
||||||
"rcpt_pay_mthd": "Pay Method",
|
|
||||||
"transid": "TransID",
|
|
||||||
"rcpt_input_mthd": "IMode",
|
|
||||||
"emvtsi": "TSI",
|
|
||||||
"emvac": "AC",
|
|
||||||
"rcpt_trans_type": "TranType",
|
|
||||||
"emvapplicationname": "PApp",
|
|
||||||
"visarewards": "RewardsProg"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"receipttoken": "H4sIAAAAAAAAACXMTQuCMBgA4P/ynh3tw_3dBI/ipQ8NOtRN53QiblpBRfTfCzo/8LwhxGAdZCCwFYoJJFQjI2kvHdGu74lVkgmrWyWNhASW5jW7cB87yHjKKePGJODnxrrnMl7dDTKmEJlSOqV/_N30XPpyj2Eddq57_KKZ8FLzmh_G1VQnVfhjiXGK1XYTc/h8AVOkf4qUAAAA",
|
|
||||||
"call": "card_payment",
|
|
||||||
"nonce": "488b5568-b5c1-4f38-8b2f-3b050f3abb11P",
|
|
||||||
"hmac": "JyPAJ9Yx0SlYBTtqns1OxAFRt+xF3l2UiLPO5zTDRBE=",
|
|
||||||
"paymentreferenceid": "C19607899P24201299",
|
|
||||||
"cardnum": "...1111",
|
|
||||||
"email": "",
|
|
||||||
"nameOnCard": "John Allen",
|
|
||||||
"cardType": "visa"
|
|
||||||
}
|
|
||||||
9
client/src/components/_test/test.component.jsx
Normal file
9
client/src/components/_test/test.component.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
export default function Test() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<QboAuthorizeComponent />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Button } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
setRefundPaymentContext: (context) =>
|
|
||||||
dispatch(setModalContext({ context: context, modal: "refund_payment" })),
|
|
||||||
});
|
|
||||||
|
|
||||||
function Test({ setRefundPaymentContext, refundPaymentModal }) {
|
|
||||||
console.log("refundPaymentModal", refundPaymentModal);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
setRefundPaymentContext({
|
|
||||||
context: {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Open Modal
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
|
||||||
@@ -1,21 +1,20 @@
|
|||||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
import { Input, Table, Checkbox, Card, Space } from "antd";
|
||||||
import queryString from "query-string";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
|
||||||
import { pageLimit } from "../../utils/config";
|
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
|
||||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
|
||||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
|
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -138,6 +137,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
title: t("exportlogs.labels.attempts"),
|
title: t("exportlogs.labels.attempts"),
|
||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
key: "attempts",
|
key: "attempts",
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<ExportLogsCountDisplay logs={record.exportlogs} />
|
<ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
),
|
),
|
||||||
@@ -146,6 +146,8 @@ export function AccountingPayablesTableComponent({
|
|||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<PayableExportButton
|
<PayableExportButton
|
||||||
billId={record.id}
|
billId={record.id}
|
||||||
@@ -208,7 +210,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{ position: "top", pageSize: 50 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -8,12 +8,9 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, {
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../owner-name-display/owner-name-display.component";
|
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||||
@@ -77,11 +74,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
dataIndex: "owner",
|
dataIndex: "owner",
|
||||||
key: "owner",
|
key: "owner",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) =>
|
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||||
alphaSort(
|
|
||||||
OwnerNameDisplayFunction(a.job),
|
|
||||||
OwnerNameDisplayFunction(b.job)
|
|
||||||
),
|
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
@@ -100,9 +93,6 @@ export function AccountingPayablesTableComponent({
|
|||||||
title: t("payments.fields.amount"),
|
title: t("payments.fields.amount"),
|
||||||
dataIndex: "amount",
|
dataIndex: "amount",
|
||||||
key: "amount",
|
key: "amount",
|
||||||
sorter: (a, b) => a.amount - b.amount,
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
|
||||||
),
|
),
|
||||||
@@ -117,25 +107,27 @@ export function AccountingPayablesTableComponent({
|
|||||||
dataIndex: "transactionid",
|
dataIndex: "transactionid",
|
||||||
key: "transactionid",
|
key: "transactionid",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("payments.fields.stripeid"),
|
||||||
|
dataIndex: "stripeid",
|
||||||
|
key: "stripeid",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.created_at"),
|
title: t("payments.fields.created_at"),
|
||||||
dataIndex: "created_at",
|
dataIndex: "created_at",
|
||||||
key: "created_at",
|
key: "created_at",
|
||||||
sorter: (a, b) => dateSort(a.created_at, b.created_at),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// title: t("payments.fields.exportedat"),
|
title: t("payments.fields.exportedat"),
|
||||||
// dataIndex: "exportedat",
|
dataIndex: "exportedat",
|
||||||
// key: "exportedat",
|
key: "exportedat",
|
||||||
// render: (text, record) => (
|
render: (text, record) => (
|
||||||
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||||
// ),
|
),
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
title: t("exportlogs.labels.attempts"),
|
title: t("exportlogs.labels.attempts"),
|
||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
@@ -149,6 +141,8 @@ export function AccountingPayablesTableComponent({
|
|||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<PaymentExportButton
|
<PaymentExportButton
|
||||||
paymentId={record.id}
|
paymentId={record.id}
|
||||||
@@ -220,7 +214,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{ position: "top", pageSize: 50 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
|
||||||
import OwnerNameDisplay, {
|
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../owner-name-display/owner-name-display.component";
|
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -65,7 +63,7 @@ export function AccountingReceivablesTableComponent({
|
|||||||
title: t("jobs.fields.status"),
|
title: t("jobs.fields.status"),
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
|
sorter: (a, b) => a.status - b.status,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -85,8 +83,7 @@ export function AccountingReceivablesTableComponent({
|
|||||||
title: t("jobs.fields.owner"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner",
|
dataIndex: "owner",
|
||||||
key: "owner",
|
key: "owner",
|
||||||
sorter: (a, b) =>
|
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
@@ -106,15 +103,6 @@ export function AccountingReceivablesTableComponent({
|
|||||||
dataIndex: "vehicle",
|
dataIndex: "vehicle",
|
||||||
key: "vehicle",
|
key: "vehicle",
|
||||||
ellipsis: true,
|
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) => {
|
render: (text, record) => {
|
||||||
return record.vehicleid ? (
|
return record.vehicleid ? (
|
||||||
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
<Link to={"/manage/vehicles/" + record.vehicleid}>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { alphaSort } from "../../utils/sorters";
|
|||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
export default function AuditTrailListComponent({ loading, data }) {
|
export default function AuditTrailListComponent({ loading, data }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
@@ -75,7 +74,7 @@ export default function AuditTrailListComponent({ loading, data }) {
|
|||||||
<Table
|
<Table
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
export default function EmailAuditTrailListComponent({ loading, data }) {
|
export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
@@ -54,7 +53,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
|
|||||||
<Table
|
<Table
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
|
|||||||
@@ -3,22 +3,10 @@ import { useMutation } from "@apollo/client";
|
|||||||
import { Button, notification, Popconfirm } from "antd";
|
import { Button, notification, Popconfirm } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
export default function BillDeleteButton({ bill }) {
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
|
||||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
|
|
||||||
|
|
||||||
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [deleteBill] = useMutation(DELETE_BILL);
|
const [deleteBill] = useMutation(DELETE_BILL);
|
||||||
@@ -48,13 +36,6 @@ export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
|
|||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("bills.successes.deleted") });
|
notification["success"]({ message: t("bills.successes.deleted") });
|
||||||
insertAuditTrail({
|
|
||||||
jobid: jobid,
|
|
||||||
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
|
|
||||||
type: "billdeleted",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (callback && typeof callback === "function") callback(bill.id);
|
|
||||||
} else {
|
} else {
|
||||||
//Check if it's an fkey violation.
|
//Check if it's an fkey violation.
|
||||||
const error = JSON.stringify(result.errors);
|
const error = JSON.stringify(result.errors);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Divider, Form, PageHeader, Popconfirm, Space } from "antd";
|
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import {
|
import {
|
||||||
DELETE_BILL_LINE,
|
DELETE_BILL_LINE,
|
||||||
INSERT_NEW_BILL_LINES,
|
INSERT_NEW_BILL_LINES,
|
||||||
UPDATE_BILL_LINE,
|
UPDATE_BILL_LINE
|
||||||
} from "../../graphql/bill-lines.queries";
|
} from "../../graphql/bill-lines.queries";
|
||||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
@@ -20,7 +20,6 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
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 BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
|
||||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
@@ -33,8 +32,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
@@ -150,7 +149,6 @@ export function BillDetailEditcontainer({
|
|||||||
jobid: bill.jobid,
|
jobid: bill.jobid,
|
||||||
billid: search.billid,
|
billid: search.billid,
|
||||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||||
type: "billupdated",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -164,7 +162,6 @@ export function BillDetailEditcontainer({
|
|||||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||||
|
|
||||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||||
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -179,7 +176,7 @@ export function BillDetailEditcontainer({
|
|||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<BillDetailEditReturn data={data} />
|
<BillDetailEditReturn data={data} />
|
||||||
<BillPrintButton billid={search.billid} />
|
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onConfirm={() => form.submit()}
|
onConfirm={() => form.submit()}
|
||||||
@@ -208,8 +205,8 @@ export function BillDetailEditcontainer({
|
|||||||
initialValues={transformData(data)}
|
initialValues={transformData(data)}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
>
|
>
|
||||||
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse}/>
|
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
|
||||||
{bodyshop.uselocalmediaserver ? (
|
{bodyshop.uselocalmediaserver ? (
|
||||||
<JobsDocumentsLocalGallery
|
<JobsDocumentsLocalGallery
|
||||||
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
@@ -45,7 +45,6 @@ export function BillDetailEditReturn({
|
|||||||
actions: {},
|
actions: {},
|
||||||
context: {
|
context: {
|
||||||
jobId: data.bills_by_pk.jobid,
|
jobId: data.bills_by_pk.jobid,
|
||||||
job: data.bills_by_pk.job,
|
|
||||||
vendorId: data.bills_by_pk.vendorid,
|
vendorId: data.bills_by_pk.vendorid,
|
||||||
returnFromBill: data.bills_by_pk.id,
|
returnFromBill: data.bills_by_pk.id,
|
||||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||||
@@ -174,11 +173,7 @@ export function BillDetailEditReturn({
|
|||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
||||||
data.bills_by_pk.is_credit_memo ||
|
|
||||||
data.bills_by_pk.isinhouse ||
|
|
||||||
disabled
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { Button, Checkbox, Form, Modal, Space, notification } from "antd";
|
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
|
||||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
import {
|
import {
|
||||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
UPDATE_JOB,
|
UPDATE_JOB,
|
||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
||||||
|
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
@@ -20,15 +20,15 @@ import {
|
|||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
import confirmDialog from "../../utils/asyncConfirm";
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||||
insertAuditTrail: ({ jobid, billid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
dispatch(insertAuditTrail({ jobid, billid, operation, type })),
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
const Templates = TemplateList("job_special");
|
const Templates = TemplateList("job_special");
|
||||||
@@ -94,7 +94,6 @@ function BillEnterModalContainer({
|
|||||||
location,
|
location,
|
||||||
outstanding_returns,
|
outstanding_returns,
|
||||||
inventory,
|
inventory,
|
||||||
federal_tax_exempt,
|
|
||||||
...remainingValues
|
...remainingValues
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
@@ -127,24 +126,13 @@ function BillEnterModalContainer({
|
|||||||
deductedfromlbr: deductedfromlbr,
|
deductedfromlbr: deductedfromlbr,
|
||||||
lbr_adjustment,
|
lbr_adjustment,
|
||||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||||
applicable_taxes: {
|
|
||||||
federal:
|
|
||||||
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
|
||||||
false,
|
|
||||||
state:
|
|
||||||
(i.applicable_taxes && i.applicable_taxes.state) ||
|
|
||||||
false,
|
|
||||||
local:
|
|
||||||
(i.applicable_taxes && i.applicable_taxes.local) ||
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||||
@@ -171,7 +159,6 @@ function BillEnterModalContainer({
|
|||||||
mod_lbr_ty: key,
|
mod_lbr_ty: key,
|
||||||
hours: adjustmentsToInsert[key].toFixed(1),
|
hours: adjustmentsToInsert[key].toFixed(1),
|
||||||
}),
|
}),
|
||||||
type: "jobmodifylbradj",
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,10 +305,7 @@ function BillEnterModalContainer({
|
|||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: values.jobid,
|
jobid: values.jobid,
|
||||||
billid: billId,
|
billid: billId,
|
||||||
operation: AuditTrailMapping.billposted(
|
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
|
||||||
r1.data.insert_bills.returning[0].invoice_number
|
|
||||||
),
|
|
||||||
type: "billposted",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (enterAgain) {
|
if (enterAgain) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Icon, { UploadOutlined } from "@ant-design/icons";
|
import Icon, { UploadOutlined } from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
import { MdOpenInNew } from "react-icons/md";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Divider,
|
Divider,
|
||||||
@@ -12,17 +12,14 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Upload,
|
Upload,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import moment from "moment";
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { MdOpenInNew } from "react-icons/md";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
@@ -31,6 +28,8 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import BillFormLines from "./bill-form.lines.component";
|
import BillFormLines from "./bill-form.lines.component";
|
||||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -47,11 +46,9 @@ export function BillFormComponent({
|
|||||||
loadLines,
|
loadLines,
|
||||||
billEdit,
|
billEdit,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
disableInHouse,
|
|
||||||
job,
|
job,
|
||||||
loadOutstandingReturns,
|
loadOutstandingReturns,
|
||||||
loadInventory,
|
loadInventory,
|
||||||
preferredMake,
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -61,11 +58,6 @@ export function BillFormComponent({
|
|||||||
{},
|
{},
|
||||||
bodyshop.imexshopid
|
bodyshop.imexshopid
|
||||||
);
|
);
|
||||||
const { ClosingPeriod } = useTreatments(
|
|
||||||
["ClosingPeriod"],
|
|
||||||
{},
|
|
||||||
bodyshop.imexshopid
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleVendorSelect = (props, opt) => {
|
const handleVendorSelect = (props, opt) => {
|
||||||
setDiscount(opt.discount);
|
setDiscount(opt.discount);
|
||||||
@@ -80,20 +72,6 @@ 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(() => {
|
useEffect(() => {
|
||||||
if (job) form.validateFields(["is_credit_memo"]);
|
if (job) form.validateFields(["is_credit_memo"]);
|
||||||
}, [job, form]);
|
}, [job, form]);
|
||||||
@@ -199,9 +177,8 @@ export function BillFormComponent({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<VendorSearchSelect
|
<VendorSearchSelect
|
||||||
disabled={disabled || disableInHouse}
|
disabled={disabled}
|
||||||
options={vendorAutoCompleteOptions}
|
options={vendorAutoCompleteOptions}
|
||||||
preferredMake={preferredMake}
|
|
||||||
onSelect={handleVendorSelect}
|
onSelect={handleVendorSelect}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -272,7 +249,7 @@ export function BillFormComponent({
|
|||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input disabled={disabled || disableInvNumber || disableInHouse} />
|
<Input disabled={disabled || disableInvNumber} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bills.fields.date")}
|
label={t("bills.fields.date")}
|
||||||
@@ -282,37 +259,6 @@ export function BillFormComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
({ getFieldValue }) => ({
|
|
||||||
validator(rule, value) {
|
|
||||||
if (
|
|
||||||
ClosingPeriod.treatment === "on" &&
|
|
||||||
bodyshop.accountingconfig.ClosingPeriod
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
moment(value)
|
|
||||||
.startOf("day")
|
|
||||||
.isSameOrAfter(
|
|
||||||
moment(
|
|
||||||
bodyshop.accountingconfig.ClosingPeriod[0]
|
|
||||||
).startOf("day")
|
|
||||||
) &&
|
|
||||||
moment(value)
|
|
||||||
.startOf("day")
|
|
||||||
.isSameOrBefore(
|
|
||||||
moment(
|
|
||||||
bodyshop.accountingconfig.ClosingPeriod[1]
|
|
||||||
).endOf("day")
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return Promise.resolve();
|
|
||||||
} else {
|
|
||||||
return Promise.reject(t("bills.validation.closingperiod"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormDatePicker disabled={disabled} />
|
<FormDatePicker disabled={disabled} />
|
||||||
@@ -402,16 +348,7 @@ export function BillFormComponent({
|
|||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
<Form.Item shouldUpdate span={15}>
|
||||||
<Form.Item
|
|
||||||
span={2}
|
|
||||||
label={t("bills.labels.federal_tax_exempt")}
|
|
||||||
name="federal_tax_exempt"
|
|
||||||
>
|
|
||||||
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
|
||||||
</Form.Item>
|
|
||||||
) : null}
|
|
||||||
<Form.Item shouldUpdate span={13}>
|
|
||||||
{() => {
|
{() => {
|
||||||
const values = form.getFieldsValue([
|
const values = form.getFieldsValue([
|
||||||
"billlines",
|
"billlines",
|
||||||
@@ -429,7 +366,7 @@ export function BillFormComponent({
|
|||||||
totals = CalculateBillTotal(values);
|
totals = CalculateBillTotal(values);
|
||||||
if (!!totals)
|
if (!!totals)
|
||||||
return (
|
return (
|
||||||
<div align="right">
|
<div>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t("bills.labels.subtotal")}
|
title={t("bills.labels.subtotal")}
|
||||||
@@ -505,11 +442,10 @@ export function BillFormComponent({
|
|||||||
billEdit={billEdit}
|
billEdit={billEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Divider orientation="left" style={{ display: billEdit ? "none" : null }}>
|
|
||||||
{t("documents.labels.upload")}
|
|
||||||
</Divider>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="upload"
|
name="upload"
|
||||||
|
label="Upload"
|
||||||
style={{ display: billEdit ? "none" : null }}
|
style={{ display: billEdit ? "none" : null }}
|
||||||
valuePropName="fileList"
|
valuePropName="fileList"
|
||||||
getValueFromEvent={(e) => {
|
getValueFromEvent={(e) => {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
|
||||||
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
||||||
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
|
||||||
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
|
||||||
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
|
||||||
import BillFormComponent from "./bill-form.component";
|
import BillFormComponent from "./bill-form.component";
|
||||||
|
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
||||||
|
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
||||||
|
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
||||||
|
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -22,7 +22,6 @@ export function BillFormContainer({
|
|||||||
billEdit,
|
billEdit,
|
||||||
disabled,
|
disabled,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
disableInHouse
|
|
||||||
}) {
|
}) {
|
||||||
const { Simple_Inventory } = useTreatments(
|
const { Simple_Inventory } = useTreatments(
|
||||||
["Simple_Inventory"],
|
["Simple_Inventory"],
|
||||||
@@ -58,10 +57,8 @@ export function BillFormContainer({
|
|||||||
job={lineData ? lineData.jobs_by_pk : null}
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
disableInvNumber={disableInvNumber}
|
disableInvNumber={disableInvNumber}
|
||||||
disableInHouse={disableInHouse}
|
|
||||||
loadOutstandingReturns={loadOutstandingReturns}
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
loadInventory={loadInventory}
|
loadInventory={loadInventory}
|
||||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
|
||||||
/>
|
/>
|
||||||
{!billEdit && (
|
{!billEdit && (
|
||||||
<BillCmdReturnsTableComponent
|
<BillCmdReturnsTableComponent
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button, Form,
|
||||||
Form,
|
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
Tooltip,
|
Tooltip
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -467,8 +466,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
return {
|
return {
|
||||||
key: `${field.index}fedtax`,
|
key: `${field.index}fedtax`,
|
||||||
valuePropName: "checked",
|
valuePropName: "checked",
|
||||||
initialValue:
|
initialValue: true,
|
||||||
form.getFieldValue("federal_tax_exempt") === true ? false : true,
|
|
||||||
name: [field.name, "applicable_taxes", "federal"],
|
name: [field.name, "applicable_taxes", "federal"],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Button, Space } from "antd";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
|
|
||||||
export default function BillPrintButton({ billid }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const Templates = TemplateList("job_special");
|
|
||||||
|
|
||||||
const submitHandler = async () => {
|
|
||||||
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 (
|
|
||||||
<Space wrap>
|
|
||||||
<Button loading={loading} onClick={submitHandler}>
|
|
||||||
{t("bills.labels.printlabels")}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
@@ -58,9 +58,9 @@ export function BillsListTableComponent({
|
|||||||
<EditFilled />
|
<EditFilled />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<BillDeleteButton bill={record} jobid={job.id} />
|
<BillDeleteButton bill={record} />
|
||||||
<BillDetailEditReturnComponent
|
<BillDetailEditReturnComponent
|
||||||
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
|
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||||
disabled={
|
disabled={
|
||||||
record.is_credit_memo ||
|
record.is_credit_memo ||
|
||||||
record.vendorid === bodyshop.inhousevendorid ||
|
record.vendorid === bodyshop.inhousevendorid ||
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
import GlobalSearchOs from "../global-search/global-search-os.component";
|
|
||||||
import "./breadcrumbs.styles.scss";
|
import "./breadcrumbs.styles.scss";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
breadcrumbs: selectBreadcrumbs,
|
breadcrumbs: selectBreadcrumbs,
|
||||||
@@ -17,12 +15,6 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
||||||
const { OpenSearch } = useTreatments(
|
|
||||||
["OpenSearch"],
|
|
||||||
{},
|
|
||||||
bodyshop && bodyshop.imexshopid
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="breadcrumb-container">
|
<Row className="breadcrumb-container">
|
||||||
<Col xs={24} sm={24} md={16}>
|
<Col xs={24} sm={24} md={16}>
|
||||||
@@ -46,7 +38,7 @@ export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
|||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={8}>
|
<Col xs={24} sm={24} md={8}>
|
||||||
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
|
<GlobalSearch />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CalculatorFilled } from "@ant-design/icons";
|
|
||||||
import { Button, Form, InputNumber, Popover, Space } from "antd";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Button, Form, InputNumber, Popover } from "antd";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CalculatorFilled } from "@ant-design/icons";
|
||||||
export default function CABCpvrtCalculator({ disabled, form }) {
|
export default function CABCpvrtCalculator({ disabled, form }) {
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
@@ -10,10 +10,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
|
||||||
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
|
||||||
});
|
|
||||||
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
|
|
||||||
setVisibility(false);
|
setVisibility(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,14 +23,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
<Form.Item name="days" label={t("jobs.labels.ca_bc_pvrt.days")}>
|
||||||
<InputNumber precision={0} min={0} />
|
<InputNumber precision={0} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
<Button type="primary" htmlType="submit">
|
||||||
<Space>
|
{t("general.actions.calculate")}
|
||||||
<Button type="primary" htmlType="submit">
|
</Button>
|
||||||
{t("general.actions.calculate")}
|
<Button onClick={() => setVisibility(false)}>Close</Button>
|
||||||
</Button>
|
|
||||||
<Button onClick={() => setVisibility(false)}>Close</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,328 +0,0 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
|
||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
Row,
|
|
||||||
Space,
|
|
||||||
Spin,
|
|
||||||
Statistic,
|
|
||||||
notification,
|
|
||||||
} from "antd";
|
|
||||||
import axios from "axios";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import {
|
|
||||||
INSERT_PAYMENT_RESPONSE,
|
|
||||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
|
||||||
} from "../../graphql/payment_response.queries";
|
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
|
||||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
|
||||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
cardPaymentModal: selectCardPayment,
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
|
||||||
dispatch(insertAuditTrail({ jobid, operation, type })),
|
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
|
||||||
});
|
|
||||||
|
|
||||||
const CardPaymentModalComponent = ({
|
|
||||||
bodyshop,
|
|
||||||
cardPaymentModal,
|
|
||||||
toggleModalVisible,
|
|
||||||
insertAuditTrail,
|
|
||||||
}) => {
|
|
||||||
const { context, actions } = cardPaymentModal;
|
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
|
||||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [, { data, refetch, queryLoading }] = useLazyQuery(
|
|
||||||
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
|
||||||
{
|
|
||||||
variables: { jobids: [context.jobid] },
|
|
||||||
skip: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
//Initialize the intellipay window.
|
|
||||||
const SetIntellipayCallbackFunctions = () => {
|
|
||||||
console.log("*** Set IntelliPay callback functions.");
|
|
||||||
window.intellipay.runOnClose(() => {
|
|
||||||
//window.intellipay.initialize();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.intellipay.runOnApproval(async function (response) {
|
|
||||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
|
||||||
//Add a slight delay to allow the refetch to properly get the data.
|
|
||||||
setTimeout(() => {
|
|
||||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
|
||||||
actions.refetch();
|
|
||||||
setLoading(false);
|
|
||||||
toggleModalVisible();
|
|
||||||
}, 750);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.intellipay.runOnNonApproval(async function (response) {
|
|
||||||
// Mutate unsuccessful payment
|
|
||||||
|
|
||||||
const { payments } = form.getFieldsValue();
|
|
||||||
await insertPaymentResponse({
|
|
||||||
variables: {
|
|
||||||
paymentResponse: payments.map((payment) => ({
|
|
||||||
amount: payment.amount,
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
jobid: payment.jobid,
|
|
||||||
declinereason: response.declinereason,
|
|
||||||
ext_paymentid: response.paymentid.toString(),
|
|
||||||
successful: false,
|
|
||||||
response,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
payments.forEach((payment) =>
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: payment.jobid,
|
|
||||||
operation: AuditTrailMapping.failedpayment(),
|
|
||||||
type: "failedpayment",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const handleIntelliPayCharge = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
//Validate
|
|
||||||
try {
|
|
||||||
await form.validateFields();
|
|
||||||
} catch (error) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
|
||||||
bodyshop,
|
|
||||||
refresh: !!window.intellipay,
|
|
||||||
paymentSplitMeta: form.getFieldsValue(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.intellipay) {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
eval(response.data);
|
|
||||||
SetIntellipayCallbackFunctions();
|
|
||||||
window.intellipay.autoOpen();
|
|
||||||
} else {
|
|
||||||
var rg = document.createRange();
|
|
||||||
let node = rg.createContextualFragment(response.data);
|
|
||||||
document.documentElement.appendChild(node);
|
|
||||||
SetIntellipayCallbackFunctions();
|
|
||||||
window.intellipay.isAutoOpen = true;
|
|
||||||
window.intellipay.initialize();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("job_payments.notifications.error.openingip"),
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title="Card Payment">
|
|
||||||
<Spin spinning={loading}>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
layout="vertical"
|
|
||||||
initialValues={{
|
|
||||||
payments: context.jobid ? [{ jobid: context.jobid }] : [],
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form.List name={["payments"]}>
|
|
||||||
{(fields, { add, remove, move }) => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<Form.Item key={field.key}>
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
<Col span={16}>
|
|
||||||
<Form.Item
|
|
||||||
key={`${index}jobid`}
|
|
||||||
label={t("jobs.fields.ro_number")}
|
|
||||||
name={[field.name, "jobid"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<JobSearchSelectComponent
|
|
||||||
notExported={false}
|
|
||||||
clm_no
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<Form.Item
|
|
||||||
key={`${index}amount`}
|
|
||||||
label={t("payments.fields.amount")}
|
|
||||||
name={[field.name, "amount"]}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyFormItemComponent />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<DeleteFilled
|
|
||||||
style={{ margin: "1rem" }}
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("general.actions.add")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.List>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(prevValues, curValues) =>
|
|
||||||
prevValues.payments?.map((p) => p?.jobid).join() !==
|
|
||||||
curValues.payments?.map((p) => p?.jobid).join()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
|
||||||
const { payments } = form.getFieldsValue();
|
|
||||||
if (
|
|
||||||
payments?.length > 0 &&
|
|
||||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
|
||||||
) {
|
|
||||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="account"
|
|
||||||
//type="hidden"
|
|
||||||
value={
|
|
||||||
payments && data && data.jobs.length > 0
|
|
||||||
? data.jobs.map((j) => j.ro_number).join(", ")
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="email"
|
|
||||||
// type="hidden"
|
|
||||||
value={
|
|
||||||
payments && data && data.jobs.length > 0
|
|
||||||
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(prevValues, curValues) =>
|
|
||||||
prevValues.payments?.map((p) => p?.amount).join() !==
|
|
||||||
curValues.payments?.map((p) => p?.amount).join()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
const { payments } = form.getFieldsValue();
|
|
||||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
|
||||||
return acc + (val?.amount || 0);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space style={{ float: "right" }}>
|
|
||||||
<Statistic
|
|
||||||
title="Amount To Charge"
|
|
||||||
value={totalAmountToCharge}
|
|
||||||
precision={2}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="amount"
|
|
||||||
//type="hidden"
|
|
||||||
value={totalAmountToCharge?.toFixed(2)}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="ipayfield"
|
|
||||||
data-ipayname="comment"
|
|
||||||
//type="hidden"
|
|
||||||
value={btoa(JSON.stringify(payments))}
|
|
||||||
hidden
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
// data-ipayname="submit"
|
|
||||||
className="ipayfield"
|
|
||||||
loading={queryLoading || loading}
|
|
||||||
disabled={!(totalAmountToCharge > 0)}
|
|
||||||
onClick={handleIntelliPayCharge}
|
|
||||||
>
|
|
||||||
{t("job_payments.buttons.proceedtopayment")}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Spin>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(CardPaymentModalComponent);
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Button, Modal } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
|
||||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import CardPaymentModalComponent from "./card-payment-modal.component.";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
cardPaymentModal: selectCardPayment,
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
|
||||||
});
|
|
||||||
|
|
||||||
function CardPaymentModalContainer({
|
|
||||||
cardPaymentModal,
|
|
||||||
toggleModalVisible,
|
|
||||||
bodyshop,
|
|
||||||
}) {
|
|
||||||
const { visible } = cardPaymentModal;
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
toggleModalVisible();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOK = () => {
|
|
||||||
toggleModalVisible();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
open={visible}
|
|
||||||
onOk={handleOK}
|
|
||||||
onCancel={handleCancel}
|
|
||||||
footer={[
|
|
||||||
<Button key="back" onClick={handleCancel}>
|
|
||||||
{t("job_payments.buttons.goback")}
|
|
||||||
</Button>,
|
|
||||||
]}
|
|
||||||
width="80%"
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<CardPaymentModalComponent />
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(CardPaymentModalContainer);
|
|
||||||
@@ -1,19 +1,12 @@
|
|||||||
import { Badge, List, Tag } from "antd";
|
import { Badge, List, Tag } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {
|
|
||||||
AutoSizer,
|
|
||||||
CellMeasurer,
|
|
||||||
CellMeasurerCache,
|
|
||||||
List as VirtualizedList,
|
|
||||||
} from "react-virtualized";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -25,95 +18,59 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setSelectedConversation(conversationId)),
|
dispatch(setSelectedConversation(conversationId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({
|
export function ChatConversationListComponent({
|
||||||
conversationList,
|
conversationList,
|
||||||
selectedConversation,
|
selectedConversation,
|
||||||
setSelectedConversation,
|
setSelectedConversation,
|
||||||
loadMoreConversations,
|
|
||||||
}) {
|
}) {
|
||||||
const cache = new CellMeasurerCache({
|
|
||||||
fixedWidth: true,
|
|
||||||
defaultHeight: 60,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rowRenderer = ({ index, key, style, parent }) => {
|
|
||||||
const item = conversationList[index];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CellMeasurer
|
|
||||||
key={key}
|
|
||||||
cache={cache}
|
|
||||||
parent={parent}
|
|
||||||
columnIndex={0}
|
|
||||||
rowIndex={index}
|
|
||||||
>
|
|
||||||
<List.Item
|
|
||||||
onClick={() => setSelectedConversation(item.id)}
|
|
||||||
className={`chat-list-item ${
|
|
||||||
item.id === selectedConversation
|
|
||||||
? "chat-list-selected-conversation"
|
|
||||||
: null
|
|
||||||
}`}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "inline-block",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.label && <div className="chat-name">{item.label}</div>}
|
|
||||||
{item.job_conversations.length > 0 ? (
|
|
||||||
<div className="chat-name">
|
|
||||||
{item.job_conversations.map((j, idx) => (
|
|
||||||
<div key={idx}>
|
|
||||||
<OwnerNameDisplay ownerObject={j.job} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "inline-block" }}>
|
|
||||||
<div>
|
|
||||||
{item.job_conversations.length > 0
|
|
||||||
? item.job_conversations.map((j, idx) => (
|
|
||||||
<Tag key={idx} className="ro-number-tag">
|
|
||||||
{j.job.ro_number}
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
|
||||||
</div>
|
|
||||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
|
||||||
</List.Item>
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<AutoSizer>
|
<List
|
||||||
{({ height, width }) => (
|
bordered
|
||||||
<VirtualizedList
|
dataSource={conversationList}
|
||||||
height={height}
|
renderItem={(item) => (
|
||||||
width={width}
|
<List.Item
|
||||||
rowCount={conversationList.length}
|
key={item.id}
|
||||||
rowHeight={cache.rowHeight}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
rowRenderer={rowRenderer}
|
className={`chat-list-item ${
|
||||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
item.id === selectedConversation
|
||||||
if (scrollTop + clientHeight === scrollHeight) {
|
? "chat-list-selected-conversation"
|
||||||
loadMoreConversations();
|
: null
|
||||||
}
|
}`}
|
||||||
}}
|
>
|
||||||
/>
|
<div sryle={{ display: "inline-block" }}>
|
||||||
|
{item.label && <div className="chat-name">{item.label}</div>}
|
||||||
|
{item.job_conversations.length > 0 ? (
|
||||||
|
<div className="chat-name">
|
||||||
|
{item.job_conversations.map((j, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
<OwnerNameDisplay ownerObject={j.job} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div sryle={{ display: "inline-block" }}>
|
||||||
|
<div>
|
||||||
|
{item.job_conversations.length > 0
|
||||||
|
? item.job_conversations.map((j, idx) => (
|
||||||
|
<Tag key={idx} className="ro-number-tag">
|
||||||
|
{j.job.ro_number}
|
||||||
|
</Tag>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
||||||
|
</div>
|
||||||
|
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||||
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
}
|
}
|
||||||
.chat-list-container {
|
.chat-list-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 1px solid gainsboro;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
@@ -22,6 +21,4 @@
|
|||||||
.ro-number-tag {
|
.ro-number-tag {
|
||||||
align-self: baseline;
|
align-self: baseline;
|
||||||
}
|
}
|
||||||
padding: 12px 24px;
|
|
||||||
border-bottom: 1px solid gainsboro;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
||||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||||
import ChatLabelComponent from "../chat-label/chat-label.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";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
|
||||||
export default function ChatConversationTitle({ conversation }) {
|
export default function ChatConversationTitle({ conversation }) {
|
||||||
@@ -14,7 +13,6 @@ export default function ChatConversationTitle({ conversation }) {
|
|||||||
{conversation && conversation.phone_num}
|
{conversation && conversation.phone_num}
|
||||||
</PhoneNumberFormatter>
|
</PhoneNumberFormatter>
|
||||||
<ChatLabelComponent conversation={conversation} />
|
<ChatLabelComponent conversation={conversation} />
|
||||||
<ChatPrintButton conversation={conversation} />
|
|
||||||
<ChatConversationTitleTags
|
<ChatConversationTitleTags
|
||||||
jobConversations={
|
jobConversations={
|
||||||
(conversation && conversation.job_conversations) || []
|
(conversation && conversation.job_conversations) || []
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||||
{
|
{
|
||||||
variables: { conversationId: selectedConversation },
|
variables: { conversationId: selectedConversation },
|
||||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: cache.identify({
|
id: cache.identify({
|
||||||
|
|||||||
@@ -8,23 +8,15 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
searchingForConversation: searchingForConversation,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatOpenButton({
|
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
|
||||||
bodyshop,
|
|
||||||
searchingForConversation,
|
|
||||||
phone,
|
|
||||||
jobid,
|
|
||||||
openChatByPhone,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|
||||||
@@ -37,7 +29,7 @@ export function ChatOpenButton({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const p = parsePhoneNumber(phone, "CA");
|
const p = parsePhoneNumber(phone, "CA");
|
||||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
|
||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,16 +4,13 @@ import {
|
|||||||
ShrinkOutlined,
|
ShrinkOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries";
|
||||||
CONVERSATION_LIST_QUERY,
|
|
||||||
UNREAD_CONVERSATION_COUNT,
|
|
||||||
} from "../../graphql/conversations.queries";
|
|
||||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||||
import {
|
import {
|
||||||
selectChatVisible,
|
selectChatVisible,
|
||||||
@@ -40,21 +37,12 @@ export function ChatPopupComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [pollInterval, setpollInterval] = useState(0);
|
const [pollInterval, setpollInterval] = useState(0);
|
||||||
|
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, {
|
||||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
...(pollInterval > 0 ? { pollInterval } : {}),
|
...(pollInterval > 0 ? { pollInterval } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [getConversations, { loading, data, refetch, fetchMore }] =
|
|
||||||
useLazyQuery(CONVERSATION_LIST_QUERY, {
|
|
||||||
fetchPolicy: "network-only",
|
|
||||||
nextFetchPolicy: "network-only",
|
|
||||||
skip: !chatVisible,
|
|
||||||
...(pollInterval > 0 ? { pollInterval } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -66,24 +54,15 @@ export function ChatPopupComponent({
|
|||||||
}, [fcmToken]);
|
}, [fcmToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatVisible)
|
if (called && chatVisible) refetch();
|
||||||
getConversations({
|
}, [chatVisible, called, refetch]);
|
||||||
variables: {
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [chatVisible, getConversations]);
|
|
||||||
|
|
||||||
const loadMoreConversations = useCallback(() => {
|
const unreadCount = data
|
||||||
if (data)
|
? data.conversations.reduce(
|
||||||
fetchMore({
|
(acc, val) => val.messages_aggregate.aggregate.count + acc,
|
||||||
variables: {
|
0
|
||||||
offset: data.conversations.length,
|
)
|
||||||
},
|
: 0;
|
||||||
});
|
|
||||||
}, [data, fetchMore]);
|
|
||||||
|
|
||||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge count={unreadCount}>
|
<Badge count={unreadCount}>
|
||||||
@@ -118,7 +97,6 @@ export function ChatPopupComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ChatConversationListComponent
|
<ChatConversationListComponent
|
||||||
conversationList={data ? data.conversations : []}
|
conversationList={data ? data.conversations : []}
|
||||||
loadMoreConversations={loadMoreConversations}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
@@ -132,7 +110,7 @@ export function ChatPopupComponent({
|
|||||||
onClick={() => toggleChatVisible()}
|
onClick={() => toggleChatVisible()}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<MessageOutlined className="chat-popup-info-icon" />
|
<MessageOutlined />
|
||||||
<strong>{t("messaging.labels.messaging")}</strong>
|
<strong>{t("messaging.labels.messaging")}</strong>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,9 +13,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.chat-popup-info-icon {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (min-width: 992px) {
|
@media only screen and (min-width: 992px) {
|
||||||
.chat-popup {
|
.chat-popup {
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
|
||||||
import { Space, Spin } from "antd";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ChatPrintButton({ conversation }) {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space wrap>
|
|
||||||
<PrinterOutlined
|
|
||||||
onClick={() => {
|
|
||||||
setLoading(true);
|
|
||||||
GenerateDocument(
|
|
||||||
{
|
|
||||||
name: TemplateList("messaging").conversation_list.key,
|
|
||||||
variables: { id: conversation.id },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
subject: TemplateList("messaging").conversation_list.subject,
|
|
||||||
},
|
|
||||||
"p",
|
|
||||||
conversation.id
|
|
||||||
);
|
|
||||||
setLoading(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MailOutlined
|
|
||||||
onClick={() => {
|
|
||||||
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 && <Spin />}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
|
|
||||||
@@ -35,15 +35,6 @@ export default function ContractsCarsComponent({
|
|||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
render: (text, record) => <div>{t(record.status)}</div>,
|
render: (text, record) => <div>{t(record.status)}</div>,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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"),
|
title: t("courtesycars.fields.year"),
|
||||||
dataIndex: "year",
|
dataIndex: "year",
|
||||||
@@ -68,14 +59,6 @@ export default function ContractsCarsComponent({
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("courtesycars.fields.color"),
|
|
||||||
dataIndex: "color",
|
|
||||||
key: "color",
|
|
||||||
sorter: (a, b) => alphaSort(a.color, b.color),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("courtesycars.fields.plate"),
|
title: t("courtesycars.fields.plate"),
|
||||||
dataIndex: "plate",
|
dataIndex: "plate",
|
||||||
@@ -110,9 +93,6 @@ export default function ContractsCarsComponent({
|
|||||||
(cc.model || "")
|
(cc.model || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(cc.color || "")
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(state.search.toLowerCase()) ||
|
|
||||||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import moment from "moment";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
||||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||||
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
|
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
|
||||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||||
@@ -68,30 +68,6 @@ export default function ContractFormComponent({
|
|||||||
<FormDateTimePicker />
|
<FormDateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{create && (
|
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(p, c) => p.scheduledreturn !== c.scheduledreturn}
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
const insuranceOver =
|
|
||||||
selectedCar &&
|
|
||||||
selectedCar.insuranceexpires &&
|
|
||||||
moment(selectedCar.insuranceexpires)
|
|
||||||
.endOf("day")
|
|
||||||
.isBefore(moment(form.getFieldValue("scheduledreturn")));
|
|
||||||
if (insuranceOver)
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
|
||||||
<span>
|
|
||||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
|
||||||
{t("contracts.labels.insuranceexpired")}
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
return <></>;
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -114,17 +90,16 @@ export default function ContractFormComponent({
|
|||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const mileageOver =
|
const mileageOver =
|
||||||
selectedCar && selectedCar.nextservicekm
|
selectedCar &&
|
||||||
? selectedCar.nextservicekm <= form.getFieldValue("kmstart")
|
selectedCar.nextservicekm <= form.getFieldValue("kmstart");
|
||||||
: false;
|
|
||||||
const dueForService =
|
const dueForService =
|
||||||
selectedCar &&
|
selectedCar &&
|
||||||
selectedCar.nextservicedate &&
|
selectedCar.nextservicedate &&
|
||||||
moment(selectedCar.nextservicedate)
|
moment(selectedCar.nextservicedate).isBefore(
|
||||||
.endOf("day")
|
moment(form.getFieldValue("scheduledreturn"))
|
||||||
.isSameOrBefore(
|
);
|
||||||
moment(form.getFieldValue("scheduledreturn"))
|
|
||||||
);
|
|
||||||
if (mileageOver || dueForService)
|
if (mileageOver || dueForService)
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||||
@@ -142,6 +117,7 @@ export default function ContractFormComponent({
|
|||||||
</span>
|
</span>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -189,9 +165,7 @@ export default function ContractFormComponent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{
|
<ContractLicenseDecodeButton form={form} />
|
||||||
//<ContractLicenseDecodeButton form={form} />
|
|
||||||
}
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
export default function ContractsJobsComponent({
|
export default function ContractsJobsComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -176,7 +175,7 @@ export default function ContractsJobsComponent({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
defaultPageSize: pageLimit,
|
defaultPageSize: 10,
|
||||||
defaultCurrent: defaultCurrent,
|
defaultCurrent: defaultCurrent,
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { useQuery } from "@apollo/client";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ContractJobsComponent from "./contract-jobs.component";
|
import ContractJobsComponent from "./contract-jobs.component";
|
||||||
|
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -15,7 +15,6 @@ export function ContractJobsContainer({ selectedJobState, bodyshop }) {
|
|||||||
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||||
variables: {
|
variables: {
|
||||||
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
||||||
isConverted: true,
|
|
||||||
},
|
},
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ import queryString from "query-string";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
|
import ContractsFindModalContainer from "../contracts-find-modal/contracts-find-modal.container";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
|
||||||
import moment from "moment";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import moment from "moment";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -210,7 +209,7 @@ export function ContractsList({
|
|||||||
}}
|
}}
|
||||||
pagination={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
pageSize: pageLimit,
|
pageSize: 25,
|
||||||
current: parseInt(page || 1),
|
current: parseInt(page || 1),
|
||||||
total: total,
|
total: total,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
export default function CourtesyCarContractListComponent({
|
export default function CourtesyCarContractListComponent({
|
||||||
contracts,
|
contracts,
|
||||||
@@ -90,7 +89,7 @@ export default function CourtesyCarContractListComponent({
|
|||||||
scroll={{ x: true }}
|
scroll={{ x: true }}
|
||||||
pagination={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
pageSize: pageLimit,
|
pageSize: 25,
|
||||||
current: parseInt(page || 1),
|
current: parseInt(page || 1),
|
||||||
total: totalContracts,
|
total: totalContracts,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -7,18 +7,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
|
import { CHECK_CC_FLEET_NUMBER } from "../../graphql/courtesy-car.queries";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
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 CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
export default function CourtesyCarCreateFormComponent({
|
export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||||
form,
|
|
||||||
saveLoading,
|
|
||||||
newCC,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
@@ -37,20 +32,8 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{newCC ? null : <FormFieldsChanged form={form} />}
|
{/* <FormFieldsChanged form={form} /> */}
|
||||||
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
|
<LayoutFormRow header={t("courtesycars.labels.vehicle")}>
|
||||||
<Form.Item
|
|
||||||
label={t("courtesycars.fields.year")}
|
|
||||||
name="year"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.make")}
|
label={t("courtesycars.fields.make")}
|
||||||
name="make"
|
name="make"
|
||||||
@@ -75,6 +58,18 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("courtesycars.fields.year")}
|
||||||
|
name="year"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.plate")}
|
label={t("courtesycars.fields.plate")}
|
||||||
name="plate"
|
name="plate"
|
||||||
@@ -123,7 +118,7 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} precision={0} />
|
<InputNumber precision={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.fleetnumber")}
|
label={t("courtesycars.fields.fleetnumber")}
|
||||||
@@ -218,27 +213,49 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
>
|
>
|
||||||
<CourtesyCarStatus />
|
<CourtesyCarStatus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
|
<Form.Item
|
||||||
<CourtesyCarReadiness />
|
label={t("courtesycars.fields.nextservicekm")}
|
||||||
|
name="nextservicekm"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<div>
|
<div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.nextservicekm")}
|
label={t("courtesycars.fields.nextservicedate")}
|
||||||
name="nextservicekm"
|
name="nextservicedate"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber min={0} precision={0} />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
shouldUpdate={(p, c) =>
|
shouldUpdate={(p, c) =>
|
||||||
p.mileage !== c.mileage || p.nextservicekm !== c.nextservicekm
|
p.mileage !== c.mileage ||
|
||||||
|
p.nextservicedate !== c.nextservicedate ||
|
||||||
|
p.nextservicekm !== c.nextservicekm
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
|
const nextservicedate = form.getFieldValue("nextservicedate");
|
||||||
const nextservicekm = form.getFieldValue("nextservicekm");
|
const nextservicekm = form.getFieldValue("nextservicekm");
|
||||||
const mileageOver = nextservicekm
|
|
||||||
? nextservicekm <= form.getFieldValue("mileage")
|
const mileageOver =
|
||||||
: false;
|
nextservicekm <= form.getFieldValue("mileage");
|
||||||
if (mileageOver)
|
|
||||||
|
const dueForService =
|
||||||
|
nextservicedate && moment(nextservicedate).isBefore(moment());
|
||||||
|
|
||||||
|
if (mileageOver || dueForService)
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||||
<span>
|
<span>
|
||||||
@@ -246,36 +263,6 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
{t("contracts.labels.cardueforservice")}
|
{t("contracts.labels.cardueforservice")}
|
||||||
</span>
|
</span>
|
||||||
<span>{`${nextservicekm} km`}</span>
|
<span>{`${nextservicekm} km`}</span>
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Form.Item
|
|
||||||
label={t("courtesycars.fields.nextservicedate")}
|
|
||||||
name="nextservicedate"
|
|
||||||
>
|
|
||||||
<FormDatePicker />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}
|
|
||||||
>
|
|
||||||
{() => {
|
|
||||||
const nextservicedate = form.getFieldValue("nextservicedate");
|
|
||||||
const dueForService =
|
|
||||||
nextservicedate &&
|
|
||||||
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
|
|
||||||
|
|
||||||
if (dueForService)
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
|
||||||
<span>
|
|
||||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
|
||||||
{t("contracts.labels.cardueforservice")}
|
|
||||||
</span>
|
|
||||||
<span>
|
<span>
|
||||||
<DateFormatter>{nextservicedate}</DateFormatter>
|
<DateFormatter>{nextservicedate}</DateFormatter>
|
||||||
</span>
|
</span>
|
||||||
@@ -296,6 +283,12 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("courtesycars.fields.registrationexpires")}
|
label={t("courtesycars.fields.registrationexpires")}
|
||||||
name="registrationexpires"
|
name="registrationexpires"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -307,8 +300,7 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
{() => {
|
{() => {
|
||||||
const expires = form.getFieldValue("registrationexpires");
|
const expires = form.getFieldValue("registrationexpires");
|
||||||
|
|
||||||
const dateover =
|
const dateover = expires && moment(expires).isBefore(moment());
|
||||||
expires && moment(expires).endOf("day").isBefore(moment());
|
|
||||||
|
|
||||||
if (dateover)
|
if (dateover)
|
||||||
return (
|
return (
|
||||||
@@ -338,13 +330,14 @@ export default function CourtesyCarCreateFormComponent({
|
|||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}
|
shouldUpdate={(p, c) =>
|
||||||
|
p.insuranceexpires !== c.insuranceexpires
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const expires = form.getFieldValue("insuranceexpires");
|
const expires = form.getFieldValue("insuranceexpires");
|
||||||
|
|
||||||
const dateover =
|
const dateover = expires && moment(expires).isBefore(moment());
|
||||||
expires && moment(expires).endOf("day").isBefore(moment());
|
|
||||||
|
|
||||||
if (dateover)
|
if (dateover)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,32 +34,6 @@ const CourtesyCarFuelComponent = (props, ref) => {
|
|||||||
step={null}
|
step={null}
|
||||||
style={{ marginLeft: "2rem", marginRight: "2rem" }}
|
style={{ marginLeft: "2rem", marginRight: "2rem" }}
|
||||||
{...props}
|
{...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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
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 (
|
|
||||||
<Select
|
|
||||||
allowClear
|
|
||||||
ref={ref}
|
|
||||||
value={option}
|
|
||||||
style={{
|
|
||||||
width: 100,
|
|
||||||
}}
|
|
||||||
onChange={setOption}
|
|
||||||
>
|
|
||||||
<Option value="courtesycars.readiness.ready">
|
|
||||||
{t("courtesycars.readiness.ready")}
|
|
||||||
</Option>
|
|
||||||
<Option value="courtesycars.readiness.notready">
|
|
||||||
{t("courtesycars.readiness.notready")}
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default forwardRef(CourtesyCarReadinessComponent);
|
|
||||||
@@ -37,9 +37,6 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
|||||||
<Option value="courtesycars.status.leasereturn">
|
<Option value="courtesycars.status.leasereturn">
|
||||||
{t("courtesycars.status.leasereturn")}
|
{t("courtesycars.status.leasereturn")}
|
||||||
</Option>
|
</Option>
|
||||||
<Option value="courtesycars.status.unavailable">
|
|
||||||
{t("courtesycars.status.unavailable")}
|
|
||||||
</Option>
|
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,26 +9,21 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import moment from "moment";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
|
import moment from "moment";
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
|
||||||
|
|
||||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" },
|
||||||
});
|
});
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [filter, setFilter] = useLocalStorage(
|
|
||||||
"filter_courtesy_cars_list",
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -55,16 +50,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
filteredValue: filter?.status || null,
|
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
text: t("courtesycars.status.in"),
|
text: t("courtesycars.status.in"),
|
||||||
value: "courtesycars.status.in",
|
value: "courtesycars.status.in",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: t("courtesycars.status.inservice"),
|
|
||||||
value: "courtesycars.status.inservice",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
text: t("courtesycars.status.out"),
|
text: t("courtesycars.status.out"),
|
||||||
value: "courtesycars.status.out",
|
value: "courtesycars.status.out",
|
||||||
@@ -77,43 +67,23 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
text: t("courtesycars.status.leasereturn"),
|
text: t("courtesycars.status.leasereturn"),
|
||||||
value: "courtesycars.status.leasereturn",
|
value: "courtesycars.status.leasereturn",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
text: t("courtesycars.status.unavailable"),
|
|
||||||
value: "courtesycars.status.unavailable",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
onFilter: (value, record) => record.status === value,
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
|
const { nextservicedate, nextservicekm, mileage } = record;
|
||||||
record;
|
|
||||||
|
|
||||||
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
|
const mileageOver = nextservicekm <= mileage;
|
||||||
|
|
||||||
const dueForService =
|
const dueForService =
|
||||||
nextservicedate &&
|
nextservicedate && moment(nextservicedate).isBefore(moment());
|
||||||
moment(nextservicedate).endOf("day").isSameOrBefore(moment());
|
|
||||||
|
|
||||||
const insuranceOver =
|
|
||||||
insuranceexpires &&
|
|
||||||
moment(insuranceexpires).endOf("day").isBefore(moment());
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
{t(record.status)}
|
{t(record.status)}
|
||||||
{(mileageOver || dueForService || insuranceOver) && (
|
{(mileageOver || dueForService) && (
|
||||||
<Tooltip
|
<Tooltip title={t("contracts.labels.cardueforservice")}>
|
||||||
title={
|
|
||||||
(mileageOver || dueForService) && insuranceOver
|
|
||||||
? t("contracts.labels.insuranceexpired") +
|
|
||||||
" / " +
|
|
||||||
t("contracts.labels.cardueforservice")
|
|
||||||
: insuranceOver
|
|
||||||
? t("contracts.labels.insuranceexpired")
|
|
||||||
: t("contracts.labels.cardueforservice")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<WarningFilled style={{ color: "tomato" }} />
|
<WarningFilled style={{ color: "tomato" }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -121,27 +91,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("courtesycars.fields.readiness"),
|
|
||||||
dataIndex: "readiness",
|
|
||||||
key: "readiness",
|
|
||||||
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
|
||||||
filteredValue: filter?.readiness || null,
|
|
||||||
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"),
|
title: t("courtesycars.fields.year"),
|
||||||
dataIndex: "year",
|
dataIndex: "year",
|
||||||
@@ -166,14 +115,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("courtesycars.fields.color"),
|
|
||||||
dataIndex: "color",
|
|
||||||
key: "color",
|
|
||||||
sorter: (a, b) => alphaSort(a.color, b.color),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("courtesycars.fields.plate"),
|
title: t("courtesycars.fields.plate"),
|
||||||
dataIndex: "plate",
|
dataIndex: "plate",
|
||||||
@@ -182,38 +123,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "plate" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("courtesycars.fields.fuel"),
|
|
||||||
dataIndex: "fuel",
|
|
||||||
key: "fuel",
|
|
||||||
sorter: (a, b) => 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 75:
|
|
||||||
return t("courtesycars.labels.fuel.34");
|
|
||||||
case 63:
|
|
||||||
return t("courtesycars.labels.fuel.58");
|
|
||||||
case 50:
|
|
||||||
return t("courtesycars.labels.fuel.12");
|
|
||||||
case 38:
|
|
||||||
return t("courtesycars.labels.fuel.38");
|
|
||||||
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"),
|
title: t("courtesycars.labels.outwith"),
|
||||||
dataIndex: "outwith",
|
dataIndex: "outwith",
|
||||||
@@ -244,8 +153,7 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({ ...state, sortedInfo: sorter });
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
setFilter(filters);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableData = searchText
|
const tableData = searchText
|
||||||
@@ -258,7 +166,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
(c.year || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
(c.year || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
(c.make || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
(c.make || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
(c.model || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
(c.model || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
(c.plate || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
|
||||||
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
|
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
|
||||||
)
|
)
|
||||||
: courtesycars;
|
: courtesycars;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import React, { useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
|
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
@@ -45,13 +44,6 @@ export default function CsiResponseFormContainer() {
|
|||||||
readOnly
|
readOnly
|
||||||
componentList={data.csi_by_pk.csiquestion.config}
|
componentList={data.csi_by_pk.csiquestion.config}
|
||||||
/>
|
/>
|
||||||
{data.csi_by_pk.validuntil ? (
|
|
||||||
<>
|
|
||||||
{t("csi.fields.validuntil")}
|
|
||||||
{": "}
|
|
||||||
<DateFormatter>{data.csi_by_pk.validuntil}</DateFormatter>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,11 +5,8 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import OwnerNameDisplay, {
|
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../owner-name-display/owner-name-display.component";
|
|
||||||
|
|
||||||
export default function CsiResponseListPaginated({
|
export default function CsiResponseListPaginated({
|
||||||
refetch,
|
refetch,
|
||||||
@@ -18,23 +15,23 @@ export default function CsiResponseListPaginated({
|
|||||||
total,
|
total,
|
||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const { responseid } = search;
|
const { responseid, page, sortcolumn, sortorder } = search;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
filteredInfo: { text: "" },
|
filteredInfo: { text: "" },
|
||||||
page: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.ro_number"),
|
title: t("jobs.fields.ro_number"),
|
||||||
dataIndex: "ro_number",
|
dataIndex: "ro_number",
|
||||||
key: "ro_number",
|
key: "ro_number",
|
||||||
|
width: "8%",
|
||||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||||
sortOrder:
|
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||||
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Link to={"/manage/jobs/" + record.job.id}>
|
<Link to={"/manage/jobs/" + record.job.id}>
|
||||||
{record.job.ro_number || t("general.labels.na")}
|
{record.job.ro_number || t("general.labels.na")}
|
||||||
@@ -43,18 +40,15 @@ export default function CsiResponseListPaginated({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.owner"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner_name",
|
dataIndex: "owner",
|
||||||
key: "owner_name",
|
key: "owner",
|
||||||
sorter: (a, b) =>
|
ellipsis: true,
|
||||||
alphaSort(
|
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||||
OwnerNameDisplayFunction(a.job),
|
width: "25%",
|
||||||
OwnerNameDisplayFunction(b.job)
|
sortOrder: sortcolumn === "owner" && sortorder,
|
||||||
),
|
|
||||||
sortOrder:
|
|
||||||
state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.job.ownerid ? (
|
return record.job.owner ? (
|
||||||
<Link to={"/manage/owners/" + record.job.ownerid}>
|
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||||
<OwnerNameDisplay ownerObject={record.job} />
|
<OwnerNameDisplay ownerObject={record.job} />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@@ -69,9 +63,9 @@ export default function CsiResponseListPaginated({
|
|||||||
dataIndex: "completedon",
|
dataIndex: "completedon",
|
||||||
key: "completedon",
|
key: "completedon",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => dateSort(a.completedon, b.completedon),
|
sorter: (a, b) => a.completedon - b.completedon,
|
||||||
sortOrder:
|
width: "25%",
|
||||||
state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
|
sortOrder: sortcolumn === "completedon" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.completedon ? (
|
return record.completedon ? (
|
||||||
<DateFormatter>{record.completedon}</DateFormatter>
|
<DateFormatter>{record.completedon}</DateFormatter>
|
||||||
@@ -81,12 +75,11 @@ export default function CsiResponseListPaginated({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
setState({
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
...state,
|
search.page = pagination.current;
|
||||||
filteredInfo: filters,
|
search.sortcolumn = sorter.columnKey;
|
||||||
sortedInfo: sorter,
|
search.sortorder = sorter.order;
|
||||||
page: pagination.current,
|
history.push({ search: queryString.stringify(search) });
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
const handleOnRowClick = (record) => {
|
||||||
@@ -113,8 +106,8 @@ export default function CsiResponseListPaginated({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
pageSize: pageLimit,
|
pageSize: 25,
|
||||||
current: parseInt(state.page || 1),
|
current: parseInt(page || 1),
|
||||||
total: total,
|
total: total,
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -128,6 +121,13 @@ export default function CsiResponseListPaginated({
|
|||||||
selectedRowKeys: [responseid],
|
selectedRowKeys: [responseid],
|
||||||
type: "radio",
|
type: "radio",
|
||||||
}}
|
}}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
}, // click row
|
||||||
|
};
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import {Card, Table, Tag} from "antd";
|
|
||||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
|
||||||
import {useTranslation} from "react-i18next";
|
|
||||||
import React, {useEffect, useState} from "react";
|
|
||||||
import moment from "moment";
|
|
||||||
import DashboardRefreshRequired from "../refresh-required.component";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const fortyFiveDaysAgo = () => moment().subtract(45, 'days').toLocaleString();
|
|
||||||
|
|
||||||
export default function JobLifecycleDashboardComponent({data, bodyshop, ...cardProps}) {
|
|
||||||
const {t} = useTranslation();
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [lifecycleData, setLifecycleData] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getLifecycleData() {
|
|
||||||
if (data && data.job_lifecycle) {
|
|
||||||
setLoading(true);
|
|
||||||
const response = await axios.post("/job/lifecycle", {
|
|
||||||
jobids: data.job_lifecycle.map(x => x.id),
|
|
||||||
statuses: bodyshop.md_ro_statuses
|
|
||||||
});
|
|
||||||
setLifecycleData(response.data.durations);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLifecycleData().catch(e => {
|
|
||||||
console.error(`Error in getLifecycleData: ${e}`);
|
|
||||||
})
|
|
||||||
}, [data, bodyshop]);
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t('job_lifecycle.columns.status'),
|
|
||||||
dataIndex: 'status',
|
|
||||||
bgColor: 'red',
|
|
||||||
key: 'status',
|
|
||||||
render: (text, record) => {
|
|
||||||
return <Tag color={record.color}>{record.status}</Tag>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('job_lifecycle.columns.human_readable'),
|
|
||||||
dataIndex: 'humanReadable',
|
|
||||||
key: 'humanReadable',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('job_lifecycle.columns.status_count'),
|
|
||||||
key: 'statusCount',
|
|
||||||
render: (text, record) => {
|
|
||||||
return lifecycleData.statusCounts[record.status];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('job_lifecycle.columns.percentage'),
|
|
||||||
dataIndex: 'percentage',
|
|
||||||
key: 'percentage',
|
|
||||||
render: (text, record) => {
|
|
||||||
return record.percentage.toFixed(2) + '%';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
if (!data.job_lifecycle || !lifecycleData) return <DashboardRefreshRequired {...cardProps} />;
|
|
||||||
|
|
||||||
const extra = `${t('job_lifecycle.content.calculated_based_on')} ${lifecycleData.jobs} ${t('job_lifecycle.content.jobs_in_since')} ${fortyFiveDaysAgo()}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card title={t("job_lifecycle.titles.dashboard")} {...cardProps}>
|
|
||||||
<LoadingSkeleton loading={loading}>
|
|
||||||
<div style={{overflow: 'scroll', height: "100%"}}>
|
|
||||||
<div id="bar-container" style={{
|
|
||||||
display: 'flex',
|
|
||||||
width: '100%',
|
|
||||||
height: '100px',
|
|
||||||
textAlign: 'center',
|
|
||||||
borderRadius: '5px',
|
|
||||||
borderWidth: '5px',
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: '#f0f2f5',
|
|
||||||
margin: 0,
|
|
||||||
padding: 0
|
|
||||||
}}>
|
|
||||||
{lifecycleData.summations.map((key, index, array) => {
|
|
||||||
const isFirst = index === 0;
|
|
||||||
const isLast = index === array.length - 1;
|
|
||||||
return (
|
|
||||||
<div key={key.status} style={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
|
|
||||||
borderTop: '1px solid #f0f2f5',
|
|
||||||
borderBottom: '1px solid #f0f2f5',
|
|
||||||
borderLeft: isFirst ? '1px solid #f0f2f5' : undefined,
|
|
||||||
borderRight: isLast ? '1px solid #f0f2f5' : undefined,
|
|
||||||
|
|
||||||
backgroundColor: key.color,
|
|
||||||
width: `${key.percentage}%`
|
|
||||||
}}
|
|
||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
|
||||||
>
|
|
||||||
|
|
||||||
{key.percentage > 15 ?
|
|
||||||
<>
|
|
||||||
<div>{key.roundedPercentage}</div>
|
|
||||||
<div style={{
|
|
||||||
backgroundColor: '#f0f2f5',
|
|
||||||
borderRadius: '5px',
|
|
||||||
paddingRight: '2px',
|
|
||||||
paddingLeft: '2px',
|
|
||||||
fontSize: '0.8rem',
|
|
||||||
}}>
|
|
||||||
{key.status}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<Card extra={extra} type='inner' title={t('job_lifecycle.content.legend_title')}
|
|
||||||
style={{marginTop: '10px'}}>
|
|
||||||
<div>
|
|
||||||
{lifecycleData.summations.map((key) => (
|
|
||||||
<Tag color={key.color} style={{width: '13vh', padding: '4px', margin: '4px'}}>
|
|
||||||
<div
|
|
||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#f0f2f5',
|
|
||||||
color: '#000',
|
|
||||||
padding: '4px',
|
|
||||||
textAlign: 'center'
|
|
||||||
}}>
|
|
||||||
{key.status} [{lifecycleData.statusCounts[key.status]}] ({key.roundedPercentage})
|
|
||||||
</div>
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
<Card style={{marginTop: "5px"}} type='inner' title={t("job_lifecycle.titles.top_durations")}>
|
|
||||||
<Table size="small" pagination={false} columns={columns}
|
|
||||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</LoadingSkeleton>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const JobLifecycleDashboardGQL = `
|
|
||||||
job_lifecycle: jobs(where: {
|
|
||||||
actual_in: {
|
|
||||||
_gte: "${moment().subtract(45, 'days').toISOString()}"
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
id
|
|
||||||
actual_in
|
|
||||||
} `;
|
|
||||||
@@ -6,7 +6,6 @@ import { alphaSort } from "../../../utils/sorters";
|
|||||||
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import DashboardRefreshRequired from "../refresh-required.component";
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
import {pageLimit} from "../../../utils/config";
|
|
||||||
|
|
||||||
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -119,7 +118,7 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
|
|||||||
<div style={{ height: "100%" }}>
|
<div style={{ height: "100%" }}>
|
||||||
<Table
|
<Table
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
scroll={{ x: true, y: "calc(100% - 4em)" }}
|
scroll={{ x: true, y: "calc(100% - 4em)" }}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
@@ -1,489 +0,0 @@
|
|||||||
import {
|
|
||||||
BranchesOutlined,
|
|
||||||
ExclamationCircleFilled,
|
|
||||||
PauseCircleOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
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, {
|
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../../owner-name-display/owner-name-display.component";
|
|
||||||
import DashboardRefreshRequired from "../refresh-required.component";
|
|
||||||
|
|
||||||
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 <DashboardRefreshRequired {...cardProps} />;
|
|
||||||
|
|
||||||
const appt = []; // Flatten Data
|
|
||||||
data.scheduled_in_today.forEach((item) => {
|
|
||||||
if (item.job) {
|
|
||||||
var i = {
|
|
||||||
canceled: item.canceled,
|
|
||||||
id: item.id,
|
|
||||||
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,
|
|
||||||
ownr_co_nm: item.job.ownr_co_nm,
|
|
||||||
ownr_ea: item.job.ownr_ea,
|
|
||||||
ownr_fn: item.job.ownr_fn,
|
|
||||||
ownr_ln: item.job.ownr_ln,
|
|
||||||
ownr_ph1: item.job.ownr_ph1,
|
|
||||||
ownr_ph2: item.job.ownr_ph2,
|
|
||||||
production_vars: item.job.production_vars,
|
|
||||||
ro_number: item.job.ro_number,
|
|
||||||
suspended: item.job.suspended,
|
|
||||||
v_make_desc: item.job.v_make_desc,
|
|
||||||
v_model_desc: item.job.v_model_desc,
|
|
||||||
v_model_yr: item.job.v_model_yr,
|
|
||||||
v_vin: item.job.v_vin,
|
|
||||||
vehicleid: item.job.vehicleid,
|
|
||||||
note: item.note,
|
|
||||||
start: item.start,
|
|
||||||
title: item.title,
|
|
||||||
};
|
|
||||||
appt.push(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
appt.sort(function (a, b) {
|
|
||||||
return new moment(a.start) - new moment(b.start);
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
<TimeFormatter>{record.start}</TimeFormatter>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Link
|
|
||||||
to={"/manage/jobs/" + record.jobid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.ro_number || t("general.labels.na")}
|
|
||||||
{record.production_vars && record.production_vars.alert ? (
|
|
||||||
<ExclamationCircleFilled className="production-alert" />
|
|
||||||
) : null}
|
|
||||||
{record.suspended && (
|
|
||||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
|
||||||
)}
|
|
||||||
{record.iouparent && (
|
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/owners/" + record.ownerid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/vehicles/" + record.vehicleid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
|
||||||
record.v_model_desc || ""
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
|
||||||
record.v_model_yr || ""
|
|
||||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.alt_transport}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.joblines_body.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.joblines_ref.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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) => <TimeFormatter>{record.start}</TimeFormatter>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Link
|
|
||||||
to={"/manage/jobs/" + record.jobid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
{record.ro_number || t("general.labels.na")}
|
|
||||||
{record.production_vars && record.production_vars.alert ? (
|
|
||||||
<ExclamationCircleFilled className="production-alert" />
|
|
||||||
) : null}
|
|
||||||
{record.suspended && (
|
|
||||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
|
||||||
)}
|
|
||||||
{record.iouparent && (
|
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/owners/" + record.ownerid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("dashboard.labels.phone"),
|
|
||||||
dataIndex: "ownr_ph",
|
|
||||||
key: "ownr_ph",
|
|
||||||
ellipsis: true,
|
|
||||||
responsive: ["md"],
|
|
||||||
render: (text, record) => (
|
|
||||||
<Space size="small" wrap>
|
|
||||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
|
||||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("jobs.fields.ownr_ea"),
|
|
||||||
dataIndex: "ownr_ea",
|
|
||||||
key: "ownr_ea",
|
|
||||||
ellipsis: true,
|
|
||||||
responsive: ["md"],
|
|
||||||
render: (text, record) => (
|
|
||||||
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/vehicles/" + record.vehicleid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
|
||||||
record.v_model_desc || ""
|
|
||||||
}`}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
|
||||||
record.v_model_desc || ""
|
|
||||||
}`}</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("jobs.fields.ins_co_nm"),
|
|
||||||
dataIndex: "ins_co_nm",
|
|
||||||
key: "ins_co_nm",
|
|
||||||
ellipsis: true,
|
|
||||||
responsive: ["md"],
|
|
||||||
sorter: (a, b) => 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,
|
|
||||||
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 = (pagination, filters, sorter) => {
|
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={t("dashboard.titles.scheduledindate", {
|
|
||||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
|
||||||
})}
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
onClick={() => setIsTvModeScheduledIn(!isTvModeScheduledIn)}
|
|
||||||
defaultChecked={isTvModeScheduledIn}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
{...cardProps}
|
|
||||||
>
|
|
||||||
<div style={{ height: "100%" }}>
|
|
||||||
<Table
|
|
||||||
onChange={handleTableChange}
|
|
||||||
pagination={false}
|
|
||||||
columns={isTvModeScheduledIn ? tvColumns : columns}
|
|
||||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
|
||||||
rowKey="id"
|
|
||||||
style={{ height: "85%" }}
|
|
||||||
dataSource={appt}
|
|
||||||
size={isTvModeScheduledIn ? "small" : "middle"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DashboardScheduledInTodayGql = `
|
|
||||||
scheduled_in_today: appointments(where: {start: {_gte: "${moment()
|
|
||||||
.startOf("day")
|
|
||||||
.toISOString()}", _lte: "${moment()
|
|
||||||
.endOf("day")
|
|
||||||
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
|
|
||||||
canceled
|
|
||||||
id
|
|
||||||
job {
|
|
||||||
alt_transport
|
|
||||||
clm_no
|
|
||||||
jobid: id
|
|
||||||
joblines(where: {removed: {_eq: false}}) {
|
|
||||||
mod_lb_hrs
|
|
||||||
mod_lbr_ty
|
|
||||||
}
|
|
||||||
ins_co_nm
|
|
||||||
iouparent
|
|
||||||
ownerid
|
|
||||||
ownr_co_nm
|
|
||||||
ownr_ea
|
|
||||||
ownr_fn
|
|
||||||
ownr_ln
|
|
||||||
ownr_ph1
|
|
||||||
ownr_ph2
|
|
||||||
production_vars
|
|
||||||
ro_number
|
|
||||||
suspended
|
|
||||||
v_make_desc
|
|
||||||
v_model_desc
|
|
||||||
v_model_yr
|
|
||||||
v_vin
|
|
||||||
vehicleid
|
|
||||||
}
|
|
||||||
note
|
|
||||||
start
|
|
||||||
title
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,493 +0,0 @@
|
|||||||
import {
|
|
||||||
BranchesOutlined,
|
|
||||||
ExclamationCircleFilled,
|
|
||||||
PauseCircleOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
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, {
|
|
||||||
OwnerNameDisplayFunction,
|
|
||||||
} from "../../owner-name-display/owner-name-display.component";
|
|
||||||
import DashboardRefreshRequired from "../refresh-required.component";
|
|
||||||
|
|
||||||
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 <DashboardRefreshRequired {...cardProps} />;
|
|
||||||
|
|
||||||
data.scheduled_out_today.forEach((item) => {
|
|
||||||
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 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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Link
|
|
||||||
to={"/manage/jobs/" + record.jobid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.ro_number || t("general.labels.na")}
|
|
||||||
{record.production_vars && record.production_vars.alert ? (
|
|
||||||
<ExclamationCircleFilled className="production-alert" />
|
|
||||||
) : null}
|
|
||||||
{record.suspended && (
|
|
||||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
|
||||||
)}
|
|
||||||
{record.iouparent && (
|
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/owners/" + record.ownerid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/vehicles/" + record.vehicleid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
|
||||||
record.v_model_desc || ""
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
|
||||||
record.v_model_yr || ""
|
|
||||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.alt_transport}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.status}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.joblines_body.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
|
||||||
{record.joblines_ref.toFixed(1)}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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) => (
|
|
||||||
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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) => (
|
|
||||||
<Link
|
|
||||||
to={"/manage/jobs/" + record.jobid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
{record.ro_number || t("general.labels.na")}
|
|
||||||
{record.production_vars && record.production_vars.alert ? (
|
|
||||||
<ExclamationCircleFilled className="production-alert" />
|
|
||||||
) : null}
|
|
||||||
{record.suspended && (
|
|
||||||
<PauseCircleOutlined style={{ color: "orangered" }} />
|
|
||||||
)}
|
|
||||||
{record.iouparent && (
|
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/owners/" + record.ownerid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("dashboard.labels.phone"),
|
|
||||||
dataIndex: "ownr_ph",
|
|
||||||
key: "ownr_ph",
|
|
||||||
ellipsis: true,
|
|
||||||
responsive: ["md"],
|
|
||||||
render: (text, record) => (
|
|
||||||
<Space size="small" wrap>
|
|
||||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
|
||||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("jobs.fields.ownr_ea"),
|
|
||||||
dataIndex: "ownr_ea",
|
|
||||||
key: "ownr_ea",
|
|
||||||
ellipsis: true,
|
|
||||||
responsive: ["md"],
|
|
||||||
render: (text, record) => (
|
|
||||||
<a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 ? (
|
|
||||||
<Link
|
|
||||||
to={"/manage/vehicles/" + record.vehicleid}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
|
||||||
record.v_model_desc || ""
|
|
||||||
}`}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
|
||||||
record.v_model_desc || ""
|
|
||||||
}`}</span>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("jobs.fields.ins_co_nm"),
|
|
||||||
dataIndex: "ins_co_nm",
|
|
||||||
key: "ins_co_nm",
|
|
||||||
ellipsis: true,
|
|
||||||
responsive: ["md"],
|
|
||||||
sorter: (a, b) => 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,
|
|
||||||
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 = (pagination, filters, sorter) => {
|
|
||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={t("dashboard.titles.scheduledoutdate", {
|
|
||||||
date: moment().startOf("day").format("MM/DD/YYYY"),
|
|
||||||
})}
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
onClick={() => setIsTvModeScheduledOut(!isTvModeScheduledOut)}
|
|
||||||
defaultChecked={isTvModeScheduledOut}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
{...cardProps}
|
|
||||||
>
|
|
||||||
<div style={{ height: "100%" }}>
|
|
||||||
<Table
|
|
||||||
onChange={handleTableChange}
|
|
||||||
pagination={false}
|
|
||||||
columns={isTvModeScheduledOut ? tvColumns : columns}
|
|
||||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
|
||||||
rowKey="id"
|
|
||||||
style={{ height: "85%" }}
|
|
||||||
dataSource={data.scheduled_out_today}
|
|
||||||
size={isTvModeScheduledOut ? "small" : "middle"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DashboardScheduledOutTodayGql = `
|
|
||||||
scheduled_out_today: jobs(where: {
|
|
||||||
date_invoiced: {_is_null: true},
|
|
||||||
ro_number: {_is_null: false},
|
|
||||||
voided: {_eq: false},
|
|
||||||
scheduled_completion: {_gte: "${moment().startOf("day").toISOString()}",
|
|
||||||
_lte: "${moment().endOf("day").toISOString()}"}}) {
|
|
||||||
alt_transport
|
|
||||||
clm_no
|
|
||||||
jobid: id
|
|
||||||
joblines(where: {removed: {_eq: false}}) {
|
|
||||||
mod_lb_hrs
|
|
||||||
mod_lbr_ty
|
|
||||||
}
|
|
||||||
ins_co_nm
|
|
||||||
iouparent
|
|
||||||
ownerid
|
|
||||||
ownr_co_nm
|
|
||||||
ownr_ea
|
|
||||||
ownr_fn
|
|
||||||
ownr_ln
|
|
||||||
ownr_ph1
|
|
||||||
ownr_ph2
|
|
||||||
production_vars
|
|
||||||
ro_number
|
|
||||||
scheduled_completion
|
|
||||||
status
|
|
||||||
suspended
|
|
||||||
v_make_desc
|
|
||||||
v_model_desc
|
|
||||||
v_model_yr
|
|
||||||
v_vin
|
|
||||||
vehicleid
|
|
||||||
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,391 +1,354 @@
|
|||||||
import Icon, {SyncOutlined} from "@ant-design/icons";
|
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||||
import {gql, useMutation, useQuery} from "@apollo/client";
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
import {Button, Dropdown, Menu, notification, PageHeader, Space} from "antd";
|
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, {useState} from "react";
|
import React, { useState } from "react";
|
||||||
import {Responsive, WidthProvider} from "react-grid-layout";
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {MdClose} from "react-icons/md";
|
import { MdClose } from "react-icons/md";
|
||||||
import {connect} from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {createStructuredSelector} from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {logImEXEvent} from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {UPDATE_DASHBOARD_LAYOUT} from "../../graphql/user.queries";
|
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||||
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import DashboardMonthlyEmployeeEfficiency, {
|
import DashboardMonthlyEmployeeEfficiency, {
|
||||||
DashboardMonthlyEmployeeEfficiencyGql,
|
DashboardMonthlyEmployeeEfficiencyGql,
|
||||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
||||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
||||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
||||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
||||||
import DashboardMonthlyRevenueGraph, {
|
import DashboardMonthlyRevenueGraph, {
|
||||||
DashboardMonthlyRevenueGraphGql,
|
DashboardMonthlyRevenueGraphGql,
|
||||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
||||||
import DashboardProjectedMonthlySales, {
|
import DashboardProjectedMonthlySales, {
|
||||||
DashboardProjectedMonthlySalesGql,
|
DashboardProjectedMonthlySalesGql,
|
||||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
||||||
import DashboardTotalProductionDollars
|
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
||||||
from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
|
||||||
import DashboardTotalProductionHours, {
|
import DashboardTotalProductionHours, {
|
||||||
DashboardTotalProductionHoursGql,
|
DashboardTotalProductionHoursGql,
|
||||||
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
} from "../dashboard-components/total-production-hours/total-production-hours.component";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
//Combination of the following:
|
//Combination of the following:
|
||||||
// /node_modules/react-grid-layout/css/styles.css
|
// /node_modules/react-grid-layout/css/styles.css
|
||||||
// /node_modules/react-resizable/css/styles.css
|
// /node_modules/react-resizable/css/styles.css
|
||||||
import DashboardScheduledInToday, {
|
|
||||||
DashboardScheduledInTodayGql,
|
|
||||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component";
|
|
||||||
import DashboardScheduledOutToday, {
|
|
||||||
DashboardScheduledOutTodayGql,
|
|
||||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component";
|
|
||||||
import JobLifecycleDashboardComponent, {
|
|
||||||
JobLifecycleDashboardGQL
|
|
||||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component";
|
|
||||||
import "./dashboard-grid.styles.scss";
|
import "./dashboard-grid.styles.scss";
|
||||||
import {GenerateDashboardData} from "./dashboard-grid.utils";
|
import { GenerateDashboardData } from "./dashboard-grid.utils";
|
||||||
|
|
||||||
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function DashboardGridComponent({currentUser, bodyshop}) {
|
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
...(bodyshop.associations[0].user.dashboardlayout
|
...(bodyshop.associations[0].user.dashboardlayout
|
||||||
? bodyshop.associations[0].user.dashboardlayout
|
? bodyshop.associations[0].user.dashboardlayout
|
||||||
: {items: [], layout: {}, layouts: []}),
|
: { items: [], layout: {}, layouts: [] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { loading, error, data, refetch } = useQuery(
|
||||||
|
createDashboardQuery(state),
|
||||||
|
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||||
|
|
||||||
|
const handleLayoutChange = async (layout, layouts) => {
|
||||||
|
logImEXEvent("dashboard_change_layout");
|
||||||
|
|
||||||
|
setState({ ...state, layout, layouts });
|
||||||
|
|
||||||
|
const result = await updateLayout({
|
||||||
|
variables: {
|
||||||
|
email: currentUser.email,
|
||||||
|
layout: { ...state, layout, layouts },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
if (!!result.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("dashboard.errors.updatinglayout", {
|
||||||
|
message: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRemoveComponent = (key) => {
|
||||||
|
logImEXEvent("dashboard_remove_component", { name: key });
|
||||||
|
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
||||||
|
|
||||||
const {loading, error, data, refetch} = useQuery(
|
const items = _.cloneDeep(state.items);
|
||||||
createDashboardQuery(state),
|
|
||||||
{fetchPolicy: "network-only", nextFetchPolicy: "network-only"}
|
|
||||||
);
|
|
||||||
|
|
||||||
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
items.splice(idxToRemove, 1);
|
||||||
|
setState({ ...state, items });
|
||||||
|
};
|
||||||
|
|
||||||
const handleLayoutChange = async (layout, layouts) => {
|
const handleAddComponent = (e) => {
|
||||||
logImEXEvent("dashboard_change_layout");
|
logImEXEvent("dashboard_add_component", { name: e });
|
||||||
|
setState({
|
||||||
|
...state,
|
||||||
|
items: [
|
||||||
|
...state.items,
|
||||||
|
{
|
||||||
|
i: e.key,
|
||||||
|
x: (state.items.length * 2) % (state.cols || 12),
|
||||||
|
y: 99, // puts it at the bottom
|
||||||
|
w: componentList[e.key].w || 2,
|
||||||
|
h: componentList[e.key].h || 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setState({...state, layout, layouts});
|
const dashboarddata = React.useMemo(
|
||||||
|
() => GenerateDashboardData(data),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||||
|
const addComponentOverlay = (
|
||||||
|
<Menu onClick={handleAddComponent}>
|
||||||
|
{Object.keys(componentList).map((key) => (
|
||||||
|
<Menu.Item
|
||||||
|
key={key}
|
||||||
|
value={key}
|
||||||
|
disabled={existingLayoutKeys.includes(key)}
|
||||||
|
>
|
||||||
|
{componentList[key].label}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
const result = await updateLayout({
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
variables: {
|
|
||||||
email: currentUser.email,
|
return (
|
||||||
layout: {...state, layout, layouts},
|
<div>
|
||||||
},
|
<PageHeader
|
||||||
});
|
extra={
|
||||||
if (!!result.errors) {
|
<Space>
|
||||||
notification["error"]({
|
<Button onClick={() => refetch()}>
|
||||||
message: t("dashboard.errors.updatinglayout", {
|
<SyncOutlined />
|
||||||
message: JSON.stringify(result.errors),
|
</Button>
|
||||||
}),
|
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||||
});
|
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
}
|
}
|
||||||
};
|
/>
|
||||||
const handleRemoveComponent = (key) => {
|
|
||||||
logImEXEvent("dashboard_remove_component", {name: key});
|
|
||||||
const idxToRemove = state.items.findIndex((i) => i.i === key);
|
|
||||||
|
|
||||||
const items = _.cloneDeep(state.items);
|
<ResponsiveReactGridLayout
|
||||||
|
className="layout"
|
||||||
items.splice(idxToRemove, 1);
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
setState({...state, items});
|
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||||
};
|
width="100%"
|
||||||
|
layouts={state.layouts}
|
||||||
const handleAddComponent = (e) => {
|
onLayoutChange={handleLayoutChange}
|
||||||
logImEXEvent("dashboard_add_component", {name: e});
|
// onBreakpointChange={onBreakpointChange}
|
||||||
setState({
|
>
|
||||||
...state,
|
{state.items.map((item, index) => {
|
||||||
items: [
|
const TheComponent = componentList[item.i].component;
|
||||||
...state.items,
|
return (
|
||||||
{
|
<div
|
||||||
i: e.key,
|
key={item.i}
|
||||||
x: (state.items.length * 2) % (state.cols || 12),
|
data-grid={{
|
||||||
y: 99, // puts it at the bottom
|
...item,
|
||||||
w: componentList[e.key].w || 2,
|
minH: componentList[item.i].minH || 1,
|
||||||
h: componentList[e.key].h || 2,
|
minW: componentList[item.i].minW || 1,
|
||||||
},
|
}}
|
||||||
],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const dashboarddata = React.useMemo(
|
|
||||||
() => GenerateDashboardData(data),
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
const existingLayoutKeys = state.items.map((i) => i.i);
|
|
||||||
const addComponentOverlay = (
|
|
||||||
<Menu onClick={handleAddComponent}>
|
|
||||||
{Object.keys(componentList).map((key) => (
|
|
||||||
<Menu.Item
|
|
||||||
key={key}
|
|
||||||
value={key}
|
|
||||||
disabled={existingLayoutKeys.includes(key)}
|
|
||||||
>
|
|
||||||
{componentList[key].label}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error"/>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageHeader
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Button onClick={() => refetch()}>
|
|
||||||
<SyncOutlined/>
|
|
||||||
</Button>
|
|
||||||
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
|
||||||
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ResponsiveReactGridLayout
|
|
||||||
className="layout"
|
|
||||||
breakpoints={{lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0}}
|
|
||||||
cols={{lg: 12, md: 10, sm: 6, xs: 4, xxs: 2}}
|
|
||||||
width="100%"
|
|
||||||
layouts={state.layouts}
|
|
||||||
onLayoutChange={handleLayoutChange}
|
|
||||||
// onBreakpointChange={onBreakpointChange}
|
|
||||||
>
|
>
|
||||||
{state.items.map((item, index) => {
|
<LoadingSkeleton loading={loading}>
|
||||||
const TheComponent = componentList[item.i].component;
|
<Icon
|
||||||
return (
|
component={MdClose}
|
||||||
<div
|
key={item.i}
|
||||||
key={item.i}
|
style={{
|
||||||
data-grid={{
|
position: "absolute",
|
||||||
...item,
|
zIndex: "2",
|
||||||
minH: componentList[item.i].minH || 1,
|
right: ".25rem",
|
||||||
minW: componentList[item.i].minW || 1,
|
top: ".25rem",
|
||||||
}}
|
cursor: "pointer",
|
||||||
>
|
}}
|
||||||
<LoadingSkeleton loading={loading}>
|
onClick={() => handleRemoveComponent(item.i)}
|
||||||
<Icon
|
/>
|
||||||
component={MdClose}
|
<TheComponent className="dashboard-card" data={dashboarddata} />
|
||||||
key={item.i}
|
</LoadingSkeleton>
|
||||||
style={{
|
</div>
|
||||||
position: "absolute",
|
);
|
||||||
zIndex: "2",
|
})}
|
||||||
right: ".25rem",
|
</ResponsiveReactGridLayout>
|
||||||
top: ".25rem",
|
</div>
|
||||||
cursor: "pointer",
|
);
|
||||||
}}
|
|
||||||
onClick={() => handleRemoveComponent(item.i)}
|
|
||||||
/>
|
|
||||||
<TheComponent className="dashboard-card" bodyshop={bodyshop} data={dashboarddata}/>
|
|
||||||
</LoadingSkeleton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ResponsiveReactGridLayout>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(DashboardGridComponent);
|
)(DashboardGridComponent);
|
||||||
|
|
||||||
const componentList = {
|
const componentList = {
|
||||||
ProductionDollars: {
|
ProductionDollars: {
|
||||||
label: i18next.t("dashboard.titles.productiondollars"),
|
label: i18next.t("dashboard.titles.productiondollars"),
|
||||||
component: DashboardTotalProductionDollars,
|
component: DashboardTotalProductionDollars,
|
||||||
gqlFragment: null,
|
gqlFragment: null,
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 1,
|
h: 1,
|
||||||
minW: 2,
|
minW: 2,
|
||||||
minH: 1,
|
minH: 1,
|
||||||
},
|
},
|
||||||
ProductionHours: {
|
ProductionHours: {
|
||||||
label: i18next.t("dashboard.titles.productionhours"),
|
label: i18next.t("dashboard.titles.productionhours"),
|
||||||
component: DashboardTotalProductionHours,
|
component: DashboardTotalProductionHours,
|
||||||
gqlFragment: DashboardTotalProductionHoursGql,
|
gqlFragment: DashboardTotalProductionHoursGql,
|
||||||
w: 3,
|
w: 3,
|
||||||
h: 1,
|
h: 1,
|
||||||
minW: 3,
|
minW: 3,
|
||||||
minH: 1,
|
minH: 1,
|
||||||
},
|
},
|
||||||
ProjectedMonthlySales: {
|
ProjectedMonthlySales: {
|
||||||
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
label: i18next.t("dashboard.titles.projectedmonthlysales"),
|
||||||
component: DashboardProjectedMonthlySales,
|
component: DashboardProjectedMonthlySales,
|
||||||
gqlFragment: DashboardProjectedMonthlySalesGql,
|
gqlFragment: DashboardProjectedMonthlySalesGql,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 1,
|
h: 1,
|
||||||
minW: 2,
|
minW: 2,
|
||||||
minH: 1,
|
minH: 1,
|
||||||
},
|
},
|
||||||
MonthlyRevenueGraph: {
|
MonthlyRevenueGraph: {
|
||||||
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
|
||||||
component: DashboardMonthlyRevenueGraph,
|
component: DashboardMonthlyRevenueGraph,
|
||||||
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
gqlFragment: DashboardMonthlyRevenueGraphGql,
|
||||||
w: 4,
|
w: 4,
|
||||||
h: 2,
|
h: 2,
|
||||||
minW: 4,
|
minW: 4,
|
||||||
minH: 2,
|
minH: 2,
|
||||||
},
|
},
|
||||||
MonthlyJobCosting: {
|
MonthlyJobCosting: {
|
||||||
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
label: i18next.t("dashboard.titles.monthlyjobcosting"),
|
||||||
component: DashboardMonthlyJobCosting,
|
component: DashboardMonthlyJobCosting,
|
||||||
gqlFragment: null,
|
gqlFragment: null,
|
||||||
minW: 6,
|
minW: 6,
|
||||||
minH: 3,
|
minH: 3,
|
||||||
w: 6,
|
w: 6,
|
||||||
h: 3,
|
h: 3,
|
||||||
},
|
},
|
||||||
MonthlyPartsSales: {
|
MonthlyPartsSales: {
|
||||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||||
component: DashboardMonthlyPartsSales,
|
component: DashboardMonthlyPartsSales,
|
||||||
gqlFragment: null,
|
gqlFragment: null,
|
||||||
minW: 2,
|
minW: 2,
|
||||||
minH: 2,
|
minH: 2,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 2,
|
h: 2,
|
||||||
},
|
},
|
||||||
MonthlyLaborSales: {
|
MonthlyLaborSales: {
|
||||||
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
||||||
component: DashboardMonthlyLaborSales,
|
component: DashboardMonthlyLaborSales,
|
||||||
gqlFragment: null,
|
gqlFragment: null,
|
||||||
minW: 2,
|
minW: 2,
|
||||||
minH: 2,
|
minH: 2,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 2,
|
h: 2,
|
||||||
},
|
},
|
||||||
// Typo in Efficency should be Efficiency, but changing it would reset users dashboard settings
|
MonthlyEmployeeEfficency: {
|
||||||
MonthlyEmployeeEfficency: {
|
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
||||||
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
|
component: DashboardMonthlyEmployeeEfficiency,
|
||||||
component: DashboardMonthlyEmployeeEfficiency,
|
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
||||||
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
|
minW: 2,
|
||||||
minW: 2,
|
minH: 2,
|
||||||
minH: 2,
|
w: 2,
|
||||||
w: 2,
|
h: 2,
|
||||||
h: 2,
|
},
|
||||||
},
|
|
||||||
ScheduleInToday: {
|
|
||||||
label: i18next.t("dashboard.titles.scheduledintoday"),
|
|
||||||
component: DashboardScheduledInToday,
|
|
||||||
gqlFragment: DashboardScheduledInTodayGql,
|
|
||||||
minW: 6,
|
|
||||||
minH: 2,
|
|
||||||
w: 10,
|
|
||||||
h: 3,
|
|
||||||
},
|
|
||||||
ScheduleOutToday: {
|
|
||||||
label: i18next.t("dashboard.titles.scheduledouttoday"),
|
|
||||||
component: DashboardScheduledOutToday,
|
|
||||||
gqlFragment: DashboardScheduledOutTodayGql,
|
|
||||||
minW: 6,
|
|
||||||
minH: 2,
|
|
||||||
w: 10,
|
|
||||||
h: 3,
|
|
||||||
},
|
|
||||||
JobLifecycle: {
|
|
||||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
|
||||||
component: JobLifecycleDashboardComponent,
|
|
||||||
gqlFragment: JobLifecycleDashboardGQL,
|
|
||||||
minW: 6,
|
|
||||||
minH: 3,
|
|
||||||
w: 6,
|
|
||||||
h: 3,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDashboardQuery = (state) => {
|
const createDashboardQuery = (state) => {
|
||||||
const componentBasedAdditions =
|
const componentBasedAdditions =
|
||||||
state &&
|
state &&
|
||||||
Array.isArray(state.layout) &&
|
Array.isArray(state.layout) &&
|
||||||
state.layout
|
state.layout
|
||||||
.map((item, index) => componentList[item.i].gqlFragment || "")
|
.map((item, index) => componentList[item.i].gqlFragment || "")
|
||||||
.join("");
|
.join("");
|
||||||
return gql`
|
return gql`
|
||||||
query QUERY_DASHBOARD_DETAILS { ${componentBasedAdditions || ""}
|
query QUERY_DASHBOARD_DETAILS {
|
||||||
monthly_sales: jobs(where: {_and: [
|
${componentBasedAdditions || ""}
|
||||||
{ voided: {_eq: false}},
|
monthly_sales: jobs(where: {_and: [
|
||||||
{date_invoiced: {_gte: "${moment()
|
{ voided: {_eq: false}},
|
||||||
.startOf("month")
|
{date_invoiced: {_gte: "${moment()
|
||||||
.startOf("day")
|
.startOf("month").startOf('day').toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
||||||
.toISOString()}"}}, {date_invoiced: {_lte: "${moment()
|
.endOf("month").endOf('day').toISOString()}"}}]}) {
|
||||||
.endOf("month")
|
id
|
||||||
.endOf("day")
|
ro_number
|
||||||
.toISOString()}"}}]}) {
|
date_invoiced
|
||||||
id
|
job_totals
|
||||||
ro_number
|
rate_la1
|
||||||
date_invoiced
|
rate_la2
|
||||||
job_totals
|
rate_la3
|
||||||
rate_la1
|
rate_la4
|
||||||
rate_la2
|
rate_laa
|
||||||
rate_la3
|
rate_lab
|
||||||
rate_la4
|
rate_lad
|
||||||
rate_laa
|
rate_lae
|
||||||
rate_lab
|
rate_laf
|
||||||
rate_lad
|
rate_lag
|
||||||
rate_lae
|
rate_lam
|
||||||
rate_laf
|
rate_lar
|
||||||
rate_lag
|
rate_las
|
||||||
rate_lam
|
rate_lau
|
||||||
rate_lar
|
rate_ma2s
|
||||||
rate_las
|
rate_ma2t
|
||||||
rate_lau
|
rate_ma3s
|
||||||
rate_ma2s
|
rate_mabl
|
||||||
rate_ma2t
|
rate_macs
|
||||||
rate_ma3s
|
rate_mahw
|
||||||
rate_mabl
|
rate_mapa
|
||||||
rate_macs
|
rate_mash
|
||||||
rate_mahw
|
rate_matd
|
||||||
rate_mapa
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
rate_mash
|
id
|
||||||
rate_matd
|
mod_lbr_ty
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
mod_lb_hrs
|
||||||
id
|
act_price
|
||||||
mod_lbr_ty
|
part_qty
|
||||||
mod_lb_hrs
|
part_type
|
||||||
act_price
|
}
|
||||||
part_qty
|
}
|
||||||
part_type
|
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
||||||
}
|
id
|
||||||
}
|
ro_number
|
||||||
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
|
ins_co_nm
|
||||||
id
|
job_totals
|
||||||
ro_number
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
ins_co_nm
|
id
|
||||||
job_totals
|
mod_lbr_ty
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
mod_lb_hrs
|
||||||
id
|
act_price
|
||||||
mod_lbr_ty
|
part_qty
|
||||||
mod_lb_hrs
|
part_type
|
||||||
act_price
|
}
|
||||||
part_qty
|
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
||||||
part_type
|
aggregate {
|
||||||
}
|
sum {
|
||||||
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }) {
|
mod_lb_hrs
|
||||||
aggregate {
|
}
|
||||||
sum {
|
}
|
||||||
mod_lb_hrs
|
}
|
||||||
}
|
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
||||||
}
|
aggregate {
|
||||||
}
|
sum {
|
||||||
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }) {
|
mod_lb_hrs
|
||||||
aggregate {
|
}
|
||||||
sum {
|
}
|
||||||
mod_lb_hrs
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
`;
|
||||||
}
|
|
||||||
}`;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
height: calc(100% - 2rem);
|
height: 80%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
// // background-color: red;
|
// // background-color: red;
|
||||||
// height: 90%;
|
// height: 90%;
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ export default function DataLabel({
|
|||||||
vertical,
|
vertical,
|
||||||
visible = true,
|
visible = true,
|
||||||
valueStyle = {},
|
valueStyle = {},
|
||||||
valueClassName,
|
|
||||||
onValueClick,
|
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
if (!visible || (hideIfNull && !!!children)) return null;
|
if (!visible || (hideIfNull && !!!children)) return null;
|
||||||
@@ -18,7 +16,7 @@ export default function DataLabel({
|
|||||||
<div {...props} style={{ display: "flex" }}>
|
<div {...props} style={{ display: "flex" }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
// flex: 2,
|
flex: 2,
|
||||||
marginRight: ".2rem",
|
marginRight: ".2rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -30,10 +28,7 @@ export default function DataLabel({
|
|||||||
marginLeft: ".3rem",
|
marginLeft: ".3rem",
|
||||||
fontWeight: "bolder",
|
fontWeight: "bolder",
|
||||||
wordWrap: "break-word",
|
wordWrap: "break-word",
|
||||||
cursor: onValueClick !== undefined ? "pointer" : "",
|
|
||||||
}}
|
}}
|
||||||
className={valueClassName}
|
|
||||||
onClick={onValueClick}
|
|
||||||
>
|
>
|
||||||
{typeof children === "string" ? (
|
{typeof children === "string" ? (
|
||||||
<Typography.Text style={valueStyle}>{children}</Typography.Text>
|
<Typography.Text style={valueStyle}>{children}</Typography.Text>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -67,7 +66,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
|
|||||||
key: "status",
|
key: "status",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("bills.fields.invoice_number"),
|
title: t("jobs.fields.ro_number"),
|
||||||
dataIndex: ["Posting", "Reference"],
|
dataIndex: ["Posting", "Reference"],
|
||||||
key: "reference",
|
key: "reference",
|
||||||
},
|
},
|
||||||
@@ -118,7 +117,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
|
rowKey={(record) => `${record.InvoiceNumber}${record.Account}`}
|
||||||
dataSource={allocationsSummary}
|
dataSource={allocationsSummary}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import {pageLimit} from "../../utils/config";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -95,7 +94,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
|||||||
<Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} />
|
<Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} />
|
||||||
)}
|
)}
|
||||||
<Table
|
<Table
|
||||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="center"
|
rowKey="center"
|
||||||
dataSource={allocationsSummary}
|
dataSource={allocationsSummary}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Statistic,
|
Statistic,
|
||||||
Switch,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
@@ -184,20 +183,6 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
<Space>
|
<Space>
|
||||||
<DmsCdkMakes form={form} socket={socket} job={job} />
|
<DmsCdkMakes form={form} socket={socket} job={job} />
|
||||||
<DmsCdkMakesRefetch />
|
<DmsCdkMakesRefetch />
|
||||||
<Form.Item
|
|
||||||
name="dms_unsold"
|
|
||||||
label={t("jobs.fields.dms.dms_unsold")}
|
|
||||||
initialValue={false}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="dms_model_override"
|
|
||||||
label={t("jobs.fields.dms.dms_model_override")}
|
|
||||||
initialValue={false}
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user