Compare commits

..

1 Commits

Author SHA1 Message Date
Dave Richer
ee8cbe33c4 Pagination Fix 2023-12-04 14:28:44 -05:00
493 changed files with 50527 additions and 80262 deletions

View File

@@ -2,8 +2,9 @@ version: 2.1
orbs: orbs:
#snyk: snyk/snyk@0.0.8 #snyk: snyk/snyk@0.0.8
#cypress: cypress-io/cypress@1.23.0 #cypress: cypress-io/cypress@1.23.0
aws-s3: circleci/aws-s3@4.0.0 aws-s3: circleci/aws-s3@2.0.0
eb: circleci/aws-elastic-beanstalk@2.0.1 eb: circleci/aws-elastic-beanstalk@1.0.2
jira: circleci/jira@1.3.1
jobs: jobs:
api-deploy: api-deploy:
docker: docker:
@@ -17,6 +18,7 @@ jobs:
eb status --verbose eb status --verbose
eb deploy eb deploy
eb status eb status
- jira/notify
hasura-migrate: hasura-migrate:
docker: docker:
@@ -46,36 +48,26 @@ jobs:
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
app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: xlarge
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build
- aws-s3/sync:
from: build
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
test-hasura-migrate: test-hasura-migrate:
docker: docker:
@@ -106,37 +98,26 @@ jobs:
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
test-app-beta-build:
docker:
- image: cimg/node:18.18.2
resource_class: snaptsoft/pfic
working_directory: ~/repo/client
steps:
- checkout:
path: ~/repo
- run:
name: Install Dependencies
command: npm i
- run: npm run build:test
- aws-s3/sync:
from: build
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
admin-app-build: admin-app-build:
docker: docker:
@@ -179,10 +160,6 @@ workflows:
filters: filters:
branches: branches:
only: master only: master
- app-beta-build:
filters:
branches:
only: master-beta
- hasura-migrate: - hasura-migrate:
secret: ${HASURA_PROD_SECRET} secret: ${HASURA_PROD_SECRET}
filters: filters:
@@ -192,10 +169,6 @@ workflows:
filters: filters:
branches: branches:
only: test only: test
- test-app-beta-build:
filters:
branches:
only: test-beta
- test-hasura-migrate: - test-hasura-migrate:
secret: ${HASURA_TEST_SECRET} secret: ${HASURA_TEST_SECRET}
filters: filters:

View File

@@ -1,13 +1,13 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql REACT_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835 REACT_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"} REACT_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715 REACT_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250 REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4' REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000 REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc

View File

@@ -1,4 +1,4 @@
GENERATE_SOURCEMAP=true GENERATE_SOURCEMAP=false
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
View File

@@ -1,3 +0,0 @@
# Sentry Config File
.sentryclirc

View File

@@ -1 +0,0 @@
legacy-peer-deps=true

View File

@@ -1,68 +1,73 @@
// 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 {convertLegacyToken} = require('@ant-design/compatible/lib'); const SentryWebpackPlugin = require("@sentry/webpack-plugin");
const {theme} = require('antd/lib');
const {defaultAlgorithm, defaultSeed} = theme;
const mapToken = defaultAlgorithm(defaultSeed);
const v4Token = convertLegacyToken(mapToken);
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
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
// include: ".",
// ignore: ["node_modules", "webpack.config.js"],
// },
// },
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {...v4Token},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => {
return {
...webpackConfig,
// Required for Dev Server
devServer: {
...webpackConfig.devServer,
allowedHosts: 'all',
},
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item; // webpack-specific configuration
}), include: ".",
}, ignore: ["node_modules", "webpack.config.js"],
}; },
},
}, },
devtool: "source-map", {
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
...(process.env.NODE_ENV === "development"
? { "@primary-color": "#a51d1d" }
: {
//"@primary-color": "#1DA57A"
}),
// "@primary-color": " #1890ff", // primary color for all components
// "@link-color": "#1890ff", // link color
// "@success-color": "#52c41a", // success state color
// "@warning-color": "#faad14", // warning state color
// "@error-color": "#f5222d", // error state color
// "@font-size-base": "14px", // major text font size
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
// "@border-radius-base": "2px", // major border radius
// "@border-color-base": "#d9d9d9", // major border color
// "@box-shadow-base":
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
},
javascriptEnabled: true,
},
},
},
},
],
webpack: {
configure: (webpackConfig) => ({
...webpackConfig,
optimization: {
...webpackConfig.optimization,
// Workaround for CircleCI bug caused by the number of CPUs shown
// https://github.com/facebook/create-react-app/issues/8320
minimizer: webpackConfig.optimization.minimizer.map((item) => {
if (item instanceof TerserPlugin) {
item.options.parallel = 2;
}
return item;
}),
},
}),
},
devtool: "source-map",
}; };

View File

@@ -1,17 +0,0 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
experimentalStudio: true,
env: {
FIREBASE_USERNAME: 'cypress@imex.test',
FIREBASE_PASSWORD: 'cypress',
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:3000',
},
})

8
client/cypress.json Normal file
View File

@@ -0,0 +1,8 @@
{
"baseUrl": "http://localhost:3000",
"experimentalStudio": true,
"env": {
"FIREBASE_USERNAME": "cypress@imex.test",
"FIREBASE_PASSWORD": "cypress"
}
}

View File

@@ -8872,13 +8872,13 @@
│ ├─ email: luis@luisrudge.net │ ├─ email: luis@luisrudge.net
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes │ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE │ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
├─ postcss-focus-open@4.0.0 ├─ postcss-focus-visible@4.0.0
│ ├─ licenses: CC0-1.0 │ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-open │ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible
│ ├─ publisher: Jonathan Neal │ ├─ publisher: Jonathan Neal
│ ├─ email: jonathantneal@hotmail.com │ ├─ email: jonathantneal@hotmail.com
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open │ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md │ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md
├─ postcss-focus-within@3.0.0 ├─ postcss-focus-within@3.0.0
│ ├─ licenses: CC0-1.0 │ ├─ licenses: CC0-1.0
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within │ ├─ repository: https://github.com/jonathantneal/postcss-focus-within

26156
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +1,100 @@
{ {
"name": "bodyshop", "name": "bodyshop",
"version": "0.2.1", "version": "0.2.1",
"engines": {
"node": "18.18.2"
},
"type": "commonjs",
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@ant-design/compatible": "^5.1.2", "@apollo/client": "^3.7.9",
"@ant-design/pro-layout": "^7.17.16",
"@apollo/client": "^3.9.0",
"@asseinfo/react-kanban": "^2.2.0", "@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^7.1.0", "@craco/craco": "^7.0.0",
"@fingerprintjs/fingerprintjs": "^4.2.2", "@fingerprintjs/fingerprintjs": "^3.4.2",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.1.0", "@sentry/react": "^7.40.0",
"@sentry/cli": "^2.27.0", "@sentry/tracing": "^7.40.0",
"@sentry/react": "^7.99.0", "@splitsoftware/splitio-react": "^1.8.1",
"@sentry/tracing": "^7.99.0", "@tanem/react-nprogress": "^5.0.8",
"@splitsoftware/splitio-react": "^1.11.0", "antd": "^4.24.8",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-legacy": "^5.3.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-refresh": "^1.3.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"antd": "^5.13.3",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0", "axios": "^1.3.4",
"axios": "^1.6.7", "craco-less": "^2.0.0",
"consola": "^3.2.3",
"dayjs": "^1.11.10",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.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",
"esbuild": "^0.20.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"firebase": "^10.7.2", "firebase": "^9.17.1",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"i18next": "^23.8.1", "i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.2", "i18next-browser-languagedetector": "^7.0.1",
"jsoneditor": "^10.0.0", "jsoneditor": "^9.9.0",
"jsreport-browser-client-dist": "^1.3.0", "jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.10.54", "libphonenumber-js": "^1.10.21",
"logrocket": "^7.0.0", "logrocket": "^3.0.1",
"markerjs2": "^2.32.0", "markerjs2": "^2.28.1",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.41",
"normalize-url": "^8.0.0", "normalize-url": "^8.0.0",
"phone": "^3.1.42", "phone": "^3.1.35",
"preval.macro": "^5.0.0", "preval.macro": "^5.0.0",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^8.1.0", "query-string": "^7.1.3",
"rc-queue-anim": "^2.0.0", "rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6", "rc-scroll-anim": "^2.7.6",
"react": "^18.2.0", "react": "^17.0.2",
"react-big-calendar": "^1.8.7", "react-big-calendar": "^1.6.8",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^7.0.2", "react-cookie": "^4.1.1",
"react-dom": "^18.2.0", "react-dom": "^17.0.2",
"react-drag-listview": "^2.0.0", "react-drag-listview": "^0.2.1",
"react-grid-gallery": "^1.0.0", "react-grid-gallery": "^1.0.0",
"react-grid-layout": "1.3.4", "react-grid-layout": "^1.3.4",
"react-i18next": "^14.0.1", "react-i18next": "^12.2.0",
"react-icons": "^5.0.1", "react-icons": "^4.7.1",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.5.3", "react-intersection-observer": "^9.4.3",
"react-markdown": "^9.0.1", "react-number-format": "^5.1.3",
"react-number-format": "^5.1.4", "react-redux": "^8.0.5",
"react-redux": "^9.1.0", "react-resizable": "^3.0.4",
"react-resizable": "^3.0.5", "react-router-dom": "^5.3.0",
"react-router-dom": "^6.21.2", "react-scripts": "^5.0.1",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-sublime-video": "^0.2.5", "react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.3",
"recharts": "^2.11.0", "recharts": "^2.4.3",
"redux": "^5.0.1", "redux": "^4.2.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.3.0", "redux-saga": "^1.2.2",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^5.1.0", "reselect": "^4.1.7",
"sass": "^1.70.0", "sass": "^1.58.3",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.6.1",
"styled-components": "^6.1.8", "styled-components": "^5.3.6",
"subscriptions-transport-ws": "^0.11.0", "subscriptions-transport-ws": "^0.11.0",
"terser-webpack-plugin": "^5.3.10", "web-vitals": "^2.1.4",
"vite-plugin-svgr": "^4.2.0", "workbox-background-sync": "^6.5.3",
"web-vitals": "^3.5.2", "workbox-broadcast-update": "^6.5.3",
"workbox-core": "^7.0.0", "workbox-cacheable-response": "^6.5.3",
"workbox-expiration": "^7.0.0", "workbox-core": "^6.5.3",
"workbox-navigation-preload": "^7.0.0", "workbox-expiration": "^6.5.3",
"workbox-precaching": "^7.0.0", "workbox-google-analytics": "^6.5.3",
"workbox-routing": "^7.0.0", "workbox-navigation-preload": "^6.5.3",
"workbox-strategies": "^7.0.0", "workbox-precaching": "^6.5.3",
"workbox-range-requests": "^6.5.3",
"workbox-routing": "^6.5.3",
"workbox-strategies": "^6.5.3",
"workbox-streams": "^6.5.3",
"yauzl": "^2.10.0" "yauzl": "^2.10.0"
}, },
"scripts": { "scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "vite", "start": "craco start",
"build": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite 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 npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'", "build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite 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 .", "madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular ."
"eulaize": "node src/utils/eulaize.js",
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@@ -131,29 +119,12 @@
"react-error-overlay": "6.0.9" "react-error-overlay": "6.0.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@sentry/webpack-plugin": "^1.20.0",
"@babel/preset-react": "^7.23.3", "@testing-library/cypress": "^8.0.3",
"@emotion/babel-plugin": "^11.11.0", "cypress": "^10.3.1",
"@emotion/react": "^11.11.3", "eslint-plugin-cypress": "^2.12.1",
"@sentry/webpack-plugin": "^2.10.3",
"@swc/core": "^1.3.107",
"@swc/plugin-styled-components": "^1.5.108",
"@testing-library/cypress": "^10.0.1",
"browserslist": "^4.22.3",
"browserslist-to-esbuild": "^2.1.1",
"craco-less": "^3.0.1",
"cross-env": "^7.0.3",
"cypress": "^13.6.4",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.6.0",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11", "react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.2"
"vite": "^5.0.11",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.19.0",
"vite-plugin-style-import": "^2.0.0"
} }
} }

View File

@@ -190,7 +190,7 @@ This package contains the following license and notice below:
# @firebase/logger # @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that This package serves as the base of all logging in the JS SDK. Any logging that
is intended to be open to Firebase end developers should go through this is intended to be visible to Firebase end developers should go through this
module. module.
## Basic Usage ## Basic Usage
@@ -9375,7 +9375,7 @@ parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying. a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently open to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2) feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the extent that warranties are provided), that licensees may convey the

View File

@@ -1029,7 +1029,7 @@ The following NPM packages may be included in this product:
- postcss-dir-pseudo-class@5.0.0 - postcss-dir-pseudo-class@5.0.0
- postcss-double-position-gradients@1.0.0 - postcss-double-position-gradients@1.0.0
- postcss-env-function@2.0.2 - postcss-env-function@2.0.2
- postcss-focus-open@4.0.0 - postcss-focus-visible@4.0.0
- postcss-focus-within@3.0.0 - postcss-focus-within@3.0.0
- postcss-gap-properties@2.0.0 - postcss-gap-properties@2.0.0
- postcss-image-set-function@3.0.1 - postcss-image-set-function@3.0.1
@@ -1699,7 +1699,7 @@ This package contains the following license and notice below:
# @firebase/logger # @firebase/logger
This package serves as the base of all logging in the JS SDK. Any logging that This package serves as the base of all logging in the JS SDK. Any logging that
is intended to be open to Firebase end developers should go through this is intended to be visible to Firebase end developers should go through this
module. module.
## Basic Usage ## Basic Usage
@@ -24029,7 +24029,7 @@ parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying. a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices" An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently open to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2) feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the extent that warranties are provided), that licensees may convey the

View File

@@ -2,12 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#002366" /> <meta name="theme-color" content="#002366" />
<meta name="description" content="ImEX Online" /> <meta name="description" content="ImEX Online" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> --> <!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<link rel="apple-touch-icon" href="public/logo192.png" /> <link rel="apple-touch-icon" href="logo192.png" />
<script type="text/javascript"> <script type="text/javascript">
window.$crisp = []; window.$crisp = [];
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e"; window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
@@ -67,7 +67,7 @@
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
@@ -82,7 +82,5 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="src/index.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,53 +1,44 @@
import {ApolloProvider} from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react'; import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
import {ConfigProvider} from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import dayjs from "../utils/day"; import moment from "moment";
import 'dayjs/locale/en';
import React from "react"; import React from "react";
import {useTranslation} from "react-i18next"; 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";
import themeProvider from "./themeProvider"; moment.locale("en-US");
dayjs.locale("en"); export const factory = SplitSdk({
core: {
authorizationKey: process.env.REACT_APP_SPLIT_API,
key: "anon",
},
});
const config = { export default function AppContainer() {
core: { const { t } = useTranslation();
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon",
},
};
export const factory = SplitSdk(config);
return (
function AppContainer() { <ApolloProvider client={client}>
const {t} = useTranslation(); <ConfigProvider
//componentSize="small"
return ( input={{ autoComplete: "new-password" }}
<ApolloProvider client={client}> locale={enLocale}
<ConfigProvider form={{
//componentSize="small" validateMessages: {
input={{autoComplete: "new-password"}} // eslint-disable-next-line no-template-curly-in-string
locale={enLocale} required: t("general.validation.required", { label: "${label}" }),
theme={themeProvider} },
form={{ }}
validateMessages: { >
// eslint-disable-next-line no-template-curly-in-string <GlobalLoadingBar />
required: t("general.validation.required", {label: "${label}"}), <SplitFactory factory={factory}>
}, <App />
}} </SplitFactory>
> </ConfigProvider>
<GlobalLoadingBar/> </ApolloProvider>
<SplitFactoryProvider factory={factory}> );
<App/>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
);
} }
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,156 +1,161 @@
import {useSplitClient} from "@splitsoftware/splitio-react"; import { useClient } from "@splitsoftware/splitio-react";
import {Button, Result} from "antd"; import { Button, Result } from "antd";
import LogRocket from "logrocket"; import LogRocket from "logrocket";
import React, {lazy, Suspense, useEffect, useState} from "react"; import React, { lazy, Suspense, useEffect } from "react";
import {useTranslation} from "react-i18next"; import { useTranslation } from "react-i18next";
import {connect} from "react-redux"; import { connect } from "react-redux";
import {Route, Routes} from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import {createStructuredSelector} from "reselect"; import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container"; import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports //Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page"; import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page"; import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container"; 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, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors"; import {
import PrivateRoute from "../components/PrivateRoute"; selectBodyshop,
selectCurrentUser,
} from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss"; import "./App.styles.scss";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
const ResetPassword = lazy(() => const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component") 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(() => const MobilePaymentContainer = lazy(() =>
import("../pages/mobile-payment/mobile-payment.container") 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,
currentEula: selectCurrentEula
}); });
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, currentEula}) { export function App({
const client = useSplitClient().client; bodyshop,
const [listenersAdded, setListenersAdded] = useState(false) checkUserSession,
const {t} = useTranslation(); currentUser,
online,
setOnline,
}) {
const client = useClient();
useEffect(() => {
useEffect(() => { if (!navigator.onLine) {
if (!navigator.onLine) { setOnline(false);
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = (e) => {
setOnline(false);
}
const onlineListener = (e) => {
setOnline(true);
}
if (!listenersAdded) {
console.log('Added events for offline and online');
window.addEventListener("offline", offlineListener);
window.addEventListener("online", onlineListener);
setListenersAdded(true);
}
return () => {
window.removeEventListener("offline", offlineListener);
window.removeEventListener("online", onlineListener);
}
}, [setOnline, listenersAdded]);
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (
client.getTreatment("LogRocket_Tracking") === "on" ||
window.location.hostname === 'beta.imex.online'
) {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
}
}, [bodyshop, client, currentUser.authorized]);
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
} }
handleBeta(); checkUserSession();
}, [checkUserSession, setOnline]);
if (!online) //const b = Grid.useBreakpoint();
return ( // console.log("Breakpoints:", b);
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
if (currentEula && !currentUser.eulaIsAccepted) { const { t } = useTranslation();
return <Eula/>
window.addEventListener("offline", function (e) {
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
useEffect(() => {
if (currentUser.authorized && bodyshop) {
client.setAttribute("imexshopid", bodyshop.imexshopid);
if (client.getTreatment("LogRocket_Tracking") === "on") {
console.log("LR Start");
LogRocket.init("gvfvfw/bodyshopapp");
}
} }
}, [bodyshop, client, currentUser.authorized]);
// Any route that is not assigned and matched will default to the Landing Page component if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
if (!online)
return ( return (
<Suspense fallback={<LoadingSpinner message="ImEX Online"/>}> <Result
<Routes> status="warning"
<Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/> title={t("general.labels.nointernet")}
<Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/> subTitle={t("general.labels.nointernet_sub")}
<Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/> extra={
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/> <Button
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/> type="primary"
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/> onClick={() => {
<Route path="/manage/*" window.location.reload();
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}> }}
<Route path="*" element={<ManagePage/>}/> >
</Route> {t("general.actions.refresh")}
<Route path="/tech/*" </Button>
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}> }
<Route path="*" element={<TechPageContainer/>}/> />
</Route>
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
<Route path="*" element={<DocumentEditorContainer/>}/>
</Route>
</Routes>
</Suspense>
); );
return (
<Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
<ErrorBoundary>
<Route exact path="/" component={LandingPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/signin" component={SignInPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/resetpassword" component={ResetPassword} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route
exact
path="/mp/:paymentIs"
component={MobilePaymentContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/manage"
component={ManagePage}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/tech"
component={TechPageContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary>
</Suspense>
</Switch>
);
} }
export default connect(mapStateToProps, mapDispatchToProps)(App); export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@@ -1,10 +1,6 @@
//Global Styles. //Global Styles.
@import "react-big-calendar/lib/sass/styles"; @import "react-big-calendar/lib/sass/styles";
.ant-menu-item-divider {
border-bottom: 1px solid #74695c !important;
}
.imex-table-header { .imex-table-header {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -147,11 +143,23 @@
} }
} }
//Update row highlighting on production board.
.ant-table-tbody > tr.ant-table-row:hover > td {
background: #e7f3ff !important;
}
.ant-table-tbody > tr.ant-table-row-selected > td {
background: #e6f7ff !important;
}
.job-line-manual { .job-line-manual {
color: tomato; color: tomato;
font-style: italic; font-style: italic;
} }
td.ant-table-column-sort {
background-color: transparent;
}
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td { .ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
background-color: #f4f4f4; background-color: #f4f4f4;

View File

@@ -1,60 +0,0 @@
import {defaultsDeep} from "lodash";
import {theme} from "antd";
const {defaultAlgorithm, darkAlgorithm} = theme;
let isDarkMode = false;
/**
* Default theme
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
*/
const defaultTheme = {
components: {
Table: {
rowHoverBg: '#e7f3ff',
rowSelectedBg: '#e6f7ff',
headerSortHoverBg: 'transparent',
},
Menu: {
darkItemHoverBg: '#1677ff',
itemHoverBg: '#1677ff',
horizontalItemHoverBg: '#1677ff',
}
},
token: {
colorPrimary: '#1677ff'
}
};
/**
* Development theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const devTheme = {
components: {
Menu: {
darkItemHoverBg: '#a51d1d',
itemHoverBg: '#a51d1d',
horizontalItemHoverBg: '#a51d1d',
}
},
token: {
colorPrimary: '#a51d1d'
}
};
/**
* Production theme
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
*/
const prodTheme = {};
const currentTheme = import.meta.env.DEV ? devTheme
: prodTheme;
const finaltheme = {
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
...defaultsDeep(currentTheme, defaultTheme)
}
export default finaltheme;

View File

@@ -1,17 +0,0 @@
import React, {useEffect} from "react";
import {Outlet, useLocation, useNavigate} from "react-router-dom";
function PrivateRoute({component: Component, isAuthorized, ...rest}) {
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if (!isAuthorized) {
navigate(`/signin?redirect=${location.pathname}`);
}
}, [isAuthorized, navigate,location]);
return <Outlet/>;
}
export default PrivateRoute;

View File

@@ -2,5 +2,5 @@ import { Alert } from "antd";
import React from "react"; import React from "react";
export default function AlertComponent(props) { export default function AlertComponent(props) {
return <Alert {...props} />; return <Alert {...props} />;
} }

View File

@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
); );
return ( return (
<Popover content={popContent} open={visibility}> <Popover content={popContent} visible={visibility}>
<Button onClick={() => setVisibility(true)}> <Button onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")} {t("allocations.actions.assign")}
</Button> </Button>

View File

@@ -59,7 +59,7 @@ export default connect(
); );
return ( return (
<Popover content={popContent} open={visibility}> <Popover content={popContent} visible={visibility}>
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>
{t("allocations.actions.assign")} {t("allocations.actions.assign")}
</Button> </Button>

View File

@@ -1,6 +1,6 @@
import {useMutation, useQuery} from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import {Button, Form, Popconfirm, Space} from "antd"; import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
import dayjs from "../../utils/day"; import moment from "moment";
import queryString from "query-string"; 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";
@@ -20,148 +20,148 @@ 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";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillDetailEditReturn from "./bill-detail-edit-return.component"; import BillDetailEditReturn from "./bill-detail-edit-return.component";
import {PageHeader} from "@ant-design/pro-layout";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
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}) => insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({jobid, operation})), dispatch(insertAuditTrail({ jobid, operation })),
}); });
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(BillDetailEditcontainer); )(BillDetailEditcontainer);
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) { export function BillDetailEditcontainer({
const search = queryString.parse(useLocation().search); setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const {t} = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [update_bill] = useMutation(UPDATE_BILL); const [update_bill] = useMutation(UPDATE_BILL);
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES); const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
const [updateBillLine] = useMutation(UPDATE_BILL_LINE); const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
const [deleteBillLine] = useMutation(DELETE_BILL_LINE); const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, { const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
variables: {billid: search.billid}, variables: { billid: search.billid },
skip: !!!search.billid, skip: !!!search.billid,
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
});
const handleSave = () => {
//It's got a previously deducted bill line!
if (
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
0
)
setVisible(true);
else {
form.submit();
}
};
const handleFinish = async (values) => {
setUpdateLoading(true);
//let adjustmentsToInsert = {};
const { billlines, upload, ...bill } = values;
const updates = [];
updates.push(
update_bill({
variables: { billId: search.billid, bill: bill },
})
);
billlines.forEach((l) => {
delete l.selected;
}); });
// ... rest of the code remains the same //Find bill lines that were deleted.
const deletedJobLines = [];
const handleSave = () => { data.bills_by_pk.billlines.forEach((a) => {
//It's got a previously deducted bill line! const matchingRecord = billlines.find((b) => b.id === a.id);
if ( if (!matchingRecord) {
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 || deletedJobLines.push(a);
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > }
0 });
)
setOpen(true);
else {
form.submit();
}
};
const handleFinish = async (values) => { deletedJobLines.forEach((d) => {
setUpdateLoading(true); updates.push(deleteBillLine({ variables: { id: d.id } }));
//let adjustmentsToInsert = {}; });
const {billlines, upload, ...bill} = values; billlines.forEach((billline) => {
const updates = []; const { deductedfromlbr, inventories, jobline, ...il } = billline;
delete il.__typename;
if (il.id) {
updates.push( updates.push(
update_bill({ updateBillLine({
variables: {billId: search.billid, bill: bill}, variables: {
}) billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
); );
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
billlines.forEach((l) => { await Promise.all(updates);
delete l.selected;
});
//Find bill lines that were deleted. insertAuditTrail({
const deletedJobLines = []; jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
data.bills_by_pk.billlines.forEach((a) => { await refetch();
const matchingRecord = billlines.find((b) => b.id === a.id); form.setFieldsValue(transformData(data));
if (!matchingRecord) { form.resetFields();
deletedJobLines.push(a); setVisible(false);
} setUpdateLoading(false);
}); };
deletedJobLines.forEach((d) => { if (error) return <AlertComponent message={error.message} type="error" />;
updates.push(deleteBillLine({variables: {id: d.id}})); if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
});
billlines.forEach((billline) => { const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
const {deductedfromlbr, inventories, jobline, ...il} = billline;
delete il.__typename;
if (il.id) {
updates.push(
updateBillLine({
variables: {
billLineId: il.id,
billLine: {
...il,
deductedfromlbr: deductedfromlbr,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
},
})
);
} else {
//It's a new line, have to insert it.
updates.push(
insertBillLine({
variables: {
billLines: [
{
...il,
deductedfromlbr: deductedfromlbr,
billid: search.billid,
joblineid: il.joblineid === "noline" ? null : il.joblineid,
},
],
},
})
);
}
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();
setOpen(false);
setUpdateLoading(false);
};
if (error) return <AlertComponent message={error.message} type="error"/>;
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
return ( return (
<> <>
@@ -176,11 +176,11 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
extra={ extra={
<Space> <Space>
<BillDetailEditReturn data={data} /> <BillDetailEditReturn data={data} />
<BillPrintButton billid={search.billid} />
<Popconfirm <Popconfirm
open={open} visible={visible}
onConfirm={() => form.submit()} onConfirm={() => form.submit()}
onCancel={() => setOpen(false)} onCancel={() => setVisible(false)}
okButtonProps={{ loading: updateLoading }} okButtonProps={{ loading: updateLoading }}
title={t("bills.labels.editadjwarning")} title={t("bills.labels.editadjwarning")}
> >
@@ -207,45 +207,45 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
> >
<BillFormContainer form={form} billEdit disabled={exported} /> <BillFormContainer form={form} billEdit disabled={exported} />
{bodyshop.uselocalmediaserver ? ( {bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery <JobsDocumentsLocalGallery
job={{id: data ? data.bills_by_pk.jobid : null}} job={{ id: data ? data.bills_by_pk.jobid : null }}
invoice_number={data ? data.bills_by_pk.invoice_number : null} invoice_number={data ? data.bills_by_pk.invoice_number : null}
vendorid={data ? data.bills_by_pk.vendorid : null} vendorid={data ? data.bills_by_pk.vendorid : null}
/> />
) : ( ) : (
<JobDocumentsGallery <JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null} jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid} billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []} documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch} billsCallback={refetch}
/> />
)}
</Form>
</>
)} )}
</Form>
</> </>
); )}
</>
);
} }
const transformData = (data) => { const transformData = (data) => {
return data return data
? { ? {
...data.bills_by_pk, ...data.bills_by_pk,
billlines: data.bills_by_pk.billlines.map((i) => { billlines: data.bills_by_pk.billlines.map((i) => {
return { return {
...i, ...i,
joblineid: !!i.joblineid ? i.joblineid : "noline", joblineid: !!i.joblineid ? i.joblineid : "noline",
applicable_taxes: { applicable_taxes: {
federal: federal:
(i.applicable_taxes && i.applicable_taxes.federal) || false, (i.applicable_taxes && i.applicable_taxes.federal) || false,
state: (i.applicable_taxes && i.applicable_taxes.state) || false, state: (i.applicable_taxes && i.applicable_taxes.state) || false,
local: (i.applicable_taxes && i.applicable_taxes.local) || false, local: (i.applicable_taxes && i.applicable_taxes.local) || false,
}, },
}; };
}), }),
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null, date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
} }
: {}; : {};
}; };

View File

@@ -3,7 +3,7 @@ import queryString from "query-string";
import React, { 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 { useLocation, useNavigate } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
@@ -33,10 +33,10 @@ export function BillDetailEditReturn({
disabled, disabled,
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const handleFinish = ({ billlines }) => { const handleFinish = ({ billlines }) => {
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
@@ -67,18 +67,18 @@ export function BillDetailEditReturn({
}); });
delete search.billid; delete search.billid;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
setOpen(false); setVisible(false);
}; };
useEffect(() => { useEffect(() => {
if (open === false) form.resetFields(); if (visible === false) form.resetFields();
}, [open, form]); }, [visible, form]);
return ( return (
<> <>
<Modal <Modal
open={open} visible={visible}
onCancel={() => setOpen(false)} onCancel={() => setVisible(false)}
destroyOnClose destroyOnClose
title={t("bills.actions.return")} title={t("bills.actions.return")}
onOk={() => form.submit()} onOk={() => form.submit()}
@@ -175,7 +175,7 @@ export function BillDetailEditReturn({
<Button <Button
disabled={data.bills_by_pk.is_credit_memo || disabled} disabled={data.bills_by_pk.is_credit_memo || disabled}
onClick={() => { onClick={() => {
setOpen(true); setVisible(true);
}} }}
> >
{t("bills.actions.return")} {t("bills.actions.return")}

View File

@@ -1,12 +1,12 @@
import { Drawer, Grid } from "antd"; import { Drawer, Grid } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import BillDetailEditComponent from "./bill-detail-edit-component"; import BillDetailEditComponent from "./bill-detail-edit-component";
export default function BillDetailEditcontainer() { export default function BillDetailEditcontainer() {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useHistory();
const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1]) .filter((screen) => !!screen[1])
@@ -29,10 +29,10 @@ export default function BillDetailEditcontainer() {
width={drawerPercentage} width={drawerPercentage}
onClose={() => { onClose={() => {
delete search.billid; delete search.billid;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
destroyOnClose destroyOnClose
open={search.billid} visible={search.billid}
> >
<BillDetailEditComponent /> <BillDetailEditComponent />
</Drawer> </Drawer>

View File

@@ -346,18 +346,18 @@ function BillEnterModalContainer({
}, [enterAgain, form]); }, [enterAgain, form]);
useEffect(() => { useEffect(() => {
if (billEnterModal.open) { if (billEnterModal.visible) {
form.setFieldsValue(formValues); form.setFieldsValue(formValues);
} else { } else {
form.resetFields(); form.resetFields();
} }
}, [billEnterModal.open, form, formValues]); }, [billEnterModal.visible, form, formValues]);
return ( return (
<Modal <Modal
title={t("bills.labels.new")} title={t("bills.labels.new")}
width={"98%"} width={"98%"}
open={billEnterModal.open} visible={billEnterModal.visible}
okText={t("general.actions.save")} okText={t("general.actions.save")}
keyboard="false" keyboard="false"
onOk={() => form.submit()} onOk={() => form.submit()}

View File

@@ -1,16 +1,26 @@
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 {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd"; import {
import dayjs from "../../utils/day"; Alert,
import React, {useEffect, useState} from "react"; Divider,
import {useTranslation} from "react-i18next"; Form,
import {MdOpenInNew} from "react-icons/md"; Input,
import {connect} from "react-redux"; Select,
import {Link} from "react-router-dom"; Space,
import {createStructuredSelector} from "reselect"; Statistic,
import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries"; Switch,
import {selectBodyshop} from "../../redux/user/user.selectors"; Upload,
} from "antd";
import moment from "moment";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { MdOpenInNew } from "react-icons/md";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
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 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";
@@ -20,307 +30,309 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; 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";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteOptions, lineData, responsibilityCenters, loadLines, billEdit, disableInvNumber, job, loadOutstandingReturns, loadInventory, preferredMake}) { export function BillFormComponent({
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
preferredMake,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const {t} = useTranslation(); const handleVendorSelect = (props, opt) => {
const client = useApolloClient(); setDiscount(opt.discount);
const [discount, setDiscount] = useState(0);
const { treatments: {Extended_Bill_Posting, ClosingPeriod} } = useSplitTreatments({ opt &&
attributes: {}, !billEdit &&
names: ["Extended_Bill_Posting", "ClosingPeriod"], loadOutstandingReturns({
splitKey: bodyshop.imexshopid, variables: {
}); jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
const handleVendorSelect = (props, opt) => { });
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
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]);
useEffect(() => { useEffect(() => {
const vendorId = form.getFieldValue("vendorid"); const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) { if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter( const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId (v) => v.id === vendorId
); );
if (matchingVendors.length === 1) { if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount); setDiscount(matchingVendors[0].discount);
} }
} }
const jobId = form.getFieldValue("jobid"); const jobId = form.getFieldValue("jobid");
if (jobId) { if (jobId) {
loadLines({variables: {id: jobId}}); loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) { if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({ loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return (
<div>
<FormFieldsChanged form={form} />
<Form.Item
style={{ display: "none" }}
name="isinhouse"
valuePropName="checked"
>
<Switch />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: { variables: {
jobId: jobId, jobId: form.getFieldValue("jobid"),
vendorId: vendorId, vendorId: form.getFieldValue("vendorid"),
}, },
}); });
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew} />
</Space>
</Link>
</Space>
} }
} />
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (vendorId === bodyshop.inhousevendorid && !billEdit) { if (response.data.bills_aggregate.aggregate.count === 0) {
loadInventory(); return Promise.resolve();
} } else if (
}, [ response.data.bills_aggregate.nodes.length === 1 &&
form, response.data.bills_aggregate.nodes[0].id ===
billEdit, form.getFieldValue("id")
loadOutstandingReturns, ) {
loadInventory, return Promise.resolve();
setDiscount, }
vendorAutoCompleteOptions, return Promise.reject(
loadLines, t("bills.validation.unique_invoice_number")
bodyshop.inhousevendorid, );
]); } else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//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} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
return ( if (
<div> !bodyshop.bill_allow_post_to_closed &&
<FormFieldsChanged form={form}/> job &&
<Form.Item (job.status === bodyshop.md_ro_statuses.default_invoiced ||
style={{display: "none"}} job.status === bodyshop.md_ro_statuses.default_exported ||
name="isinhouse" job.status === bodyshop.md_ro_statuses.default_void) &&
valuePropName="checked" (value === false || !value)
> ) {
<Switch/> return Promise.reject(t("bills.labels.onlycmforinvoiced"));
</Form.Item> }
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t("bills.fields.ro_number")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t("bills.fields.vendor")}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t("bills.labels.iouexists")}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew}/>
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t("bills.fields.invoice_number")}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
async validator(rule, value) {
const vendorid = getFieldValue("vendorid");
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id ===
form.getFieldValue("id")
) {
return Promise.resolve();
}
return Promise.reject(
t("bills.validation.unique_invoice_number")
);
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber}/>
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({getFieldValue}) => ({
validator(rule, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
dayjs(value)
.startOf("day")
.isSameOrAfter(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
dayjs(value)
.startOf("day")
.isSameOrBefore(
dayjs(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(t("bills.validation.closingperiod"));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled}/>
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({getFieldValue}) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve(); return Promise.resolve();
}, },
@@ -375,16 +387,7 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
> >
<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",
@@ -402,7 +405,7 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
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")}
@@ -460,55 +463,55 @@ export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteO
</LayoutFormRow> </LayoutFormRow>
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider> <Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
{Extended_Bill_Posting.treatment === "on" ? ( {Extended_Bill_Posting.treatment === "on" ? (
<BillFormLinesExtended <BillFormLinesExtended
lineData={lineData} lineData={lineData}
discount={discount} discount={discount}
form={form} form={form}
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
/> />
) : ( ) : (
<BillFormLines <BillFormLines
lineData={lineData} lineData={lineData}
discount={discount} discount={discount}
form={form} form={form}
responsibilityCenters={responsibilityCenters} responsibilityCenters={responsibilityCenters}
disabled={disabled} disabled={disabled}
billEdit={billEdit} billEdit={billEdit}
/> />
)} )}
<Form.Item <Form.Item
name="upload" name="upload"
label="Upload" label="Upload"
style={{display: billEdit ? "none" : null}} style={{ display: billEdit ? "none" : null }}
valuePropName="fileList" valuePropName="fileList"
getValueFromEvent={(e) => { getValueFromEvent={(e) => {
if (Array.isArray(e)) { if (Array.isArray(e)) {
return e; return e;
} }
return e && e.fileList; return e && e.fileList;
}} }}
> >
<Upload.Dragger <Upload.Dragger
multiple={true} multiple={true}
name="logo" name="logo"
beforeUpload={() => false} beforeUpload={() => false}
listType="picture" listType="picture"
> >
<> <>
<p className="ant-upload-drag-icon"> <p className="ant-upload-drag-icon">
<UploadOutlined/> <UploadOutlined />
</p> </p>
<p className="ant-upload-text"> <p className="ant-upload-text">
Click or drag files to this area to upload. Click or drag files to this area to upload.
</p> </p>
</> </>
</Upload.Dragger> </Upload.Dragger>
</Form.Item> </Form.Item>
</div> </div>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent); export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

View File

@@ -1,5 +1,5 @@
import { useLazyQuery, useQuery } from "@apollo/client"; import { useLazyQuery, useQuery } from "@apollo/client";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; 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";
@@ -23,11 +23,11 @@ export function BillFormContainer({
disabled, disabled,
disableInvNumber, disableInvNumber,
}) { }) {
const { treatments: {Simple_Inventory} } = useSplitTreatments({ const { Simple_Inventory } = useTreatments(
attributes: {}, ["Simple_Inventory"],
names: ["Simple_Inventory"], {},
splitKey: bodyshop && bodyshop.imexshopid, bodyshop && bodyshop.imexshopid
}); );
const { data: VendorAutoCompleteData } = useQuery( const { data: VendorAutoCompleteData } = useQuery(
SEARCH_VENDOR_AUTOCOMPLETE, SEARCH_VENDOR_AUTOCOMPLETE,

View File

@@ -1,15 +1,14 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import {useSplitTreatments} 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";
@@ -41,14 +40,11 @@ export function BillEnterModalLinesComponent({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const { Simple_Inventory } = useTreatments(
const { treatments: {Simple_Inventory} } = useSplitTreatments({ ["Simple_Inventory"],
attributes: {}, {},
names: ["Simple_Inventory"], bodyshop && bodyshop.imexshopid
splitKey: bodyshop && bodyshop.imexshopid, );
});
const columns = (remove) => { const columns = (remove) => {
return [ return [
{ {
@@ -470,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"],
}; };
}, },

View File

@@ -15,8 +15,7 @@ const BillLineSearchSelect = (
disabled={disabled} disabled={disabled}
ref={ref} ref={ref}
showSearch showSearch
popupMatchSelectWidth={false} dropdownMatchSelectWidth={false}
optionLabelProp={"name"}
// optionFilterProp="line_desc" // optionFilterProp="line_desc"
filterOption={(inputValue, option) => { filterOption={(inputValue, option) => {
return ( return (
@@ -58,9 +57,6 @@ const BillLineSearchSelect = (
style={{ style={{
...(item.removed ? { textDecoration: "line-through" } : {}), ...(item.removed ? { textDecoration: "line-through" } : {}),
}} }}
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
> >
<span> <span>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${ {`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${

View File

@@ -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>
);
}

View File

@@ -2,7 +2,7 @@ import { FileAddFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, notification, Tooltip } from "antd"; import { Button, notification, Tooltip } from "antd";
import { t } from "i18next"; import { t } from "i18next";
import dayjs from "./../../utils/day"; import moment from "moment";
import React, { useState } from "react"; import React, { useState } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -36,6 +36,7 @@ export function BilllineAddInventory({
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { billid } = queryString.parse(useLocation().search); const { billid } = queryString.parse(useLocation().search);
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT); const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
const addToInventory = async () => { const addToInventory = async () => {
@@ -49,7 +50,7 @@ export function BilllineAddInventory({
jobid: jobid, jobid: jobid,
isinhouse: true, isinhouse: true,
is_credit_memo: true, is_credit_memo: true,
date: dayjs().format("YYYY-MM-DD"), date: moment().format("YYYY-MM-DD"),
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate, federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate, state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate, local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
@@ -91,7 +92,7 @@ export function BilllineAddInventory({
pol: { pol: {
returnfrombill: billid, returnfrombill: billid,
vendorid: bodyshop.inhousevendorid, vendorid: bodyshop.inhousevendorid,
deliver_by: dayjs().format("YYYY-MM-DD"), deliver_by: moment().format("YYYY-MM-DD"),
parts_order_lines: { parts_order_lines: {
data: [ data: [
{ {

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import queryString from "query-string"; import queryString from "query-string";
import { useLocation, useNavigate } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { Table, Input } from "antd"; import { Table, Input } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
@@ -10,7 +10,7 @@ import AlertComponent from "../alert/alert.component";
export default function BillsVendorsList() { export default function BillsVendorsList() {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const history = useNavigate(); const history = useHistory();
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, { const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
fetchPolicy: "network-only", fetchPolicy: "network-only",

View File

@@ -1,64 +1,54 @@
import {HomeFilled} from "@ant-design/icons"; import { HomeFilled } from "@ant-design/icons";
import {Breadcrumb, Col, Row} from "antd"; import { Breadcrumb, Row, Col } from "antd";
import React from "react"; import React from "react";
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 {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 GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss"; import "./breadcrumbs.styles.scss";
import {useSplitTreatments} from "@splitsoftware/splitio-react"; import { useTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs, breadcrumbs: selectBreadcrumbs,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
export function BreadCrumbs({breadcrumbs, bodyshop}) { export function BreadCrumbs({ breadcrumbs, bodyshop }) {
const { OpenSearch } = useTreatments(
["OpenSearch"],
{},
bodyshop && bodyshop.imexshopid
);
const {treatments: {OpenSearch}} = useSplitTreatments({ return (
attributes: {}, <Row className="breadcrumb-container">
names: ["OpenSearch"], <Col xs={24} sm={24} md={16}>
splitKey: bodyshop && bodyshop.imexshopid, <Breadcrumb separator=">">
}); <Breadcrumb.Item>
// TODO - Client Update - Technically key is not doing anything here <Link to={`/manage`}>
return ( <HomeFilled />{" "}
<Row className="breadcrumb-container"> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
<Col xs={24} sm={24} md={16}> ""}
<Breadcrumb </Link>
separator=">" </Breadcrumb.Item>
items={[ {breadcrumbs.map((item) =>
{ item.link ? (
key: "home", <Breadcrumb.Item key={item.label}>
title: ( <Link to={item.link}>{item.label} </Link>
<Link to={`/manage/`}> </Breadcrumb.Item>
<HomeFilled/>{" "} ) : (
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || <Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
""} )
</Link> )}
), </Breadcrumb>
}, </Col>
...breadcrumbs.map((item) => <Col xs={24} sm={24} md={8}>
item.link {OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
? { </Col>
key: item.label, </Row>
title: <Link to={item.link}>{item.label}</Link>, );
}
: {
key: item.label,
title: item.label,
}
),
]}
/>
</Col>
<Col xs={24} sm={24} md={8}>
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
</Col>
</Row>
);
} }
export default connect(mapStateToProps, null)(BreadCrumbs); export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -25,7 +25,7 @@ export function ContractsFindModalContainer({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { open } = caBcEtfTableModal; const { visible } = caBcEtfTableModal;
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const EtfTemplate = TemplateList("special").ca_bc_etf_table; const EtfTemplate = TemplateList("special").ca_bc_etf_table;
@@ -63,14 +63,14 @@ export function ContractsFindModalContainer({
}; };
useEffect(() => { useEffect(() => {
if (open) { if (visible) {
form.resetFields(); form.resetFields();
} }
}, [open, form]); }, [visible, form]);
return ( return (
<Modal <Modal
open={open} visible={visible}
width="70%" width="70%"
title={t("payments.labels.findermodal")} title={t("payments.labels.findermodal")}
onCancel={() => toggleModalVisible()} onCancel={() => toggleModalVisible()}

View File

@@ -38,7 +38,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
<Popover <Popover
destroyTooltipOnHide destroyTooltipOnHide
content={popContent} content={popContent}
open={visibility} visible={visibility}
disabled={disabled} disabled={disabled}
> >
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>

View File

@@ -13,7 +13,7 @@ import {
notification, notification,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import dayjs from "../../utils/day"; 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 { connect } from "react-redux"; import { connect } from "react-redux";
@@ -117,7 +117,7 @@ const CardPaymentModalComponent = ({
payer: t("payments.labels.customer"), payer: t("payments.labels.customer"),
type: values.paymentResponse.cardbrand, type: values.paymentResponse.cardbrand,
jobid: payment.jobid, jobid: payment.jobid,
date: dayjs(Date.now()), date: moment(Date.now()),
payment_responses: { payment_responses: {
data: [ data: [
{ {

View File

@@ -22,7 +22,7 @@ function CardPaymentModalContainer({
toggleModalVisible, toggleModalVisible,
bodyshop, bodyshop,
}) { }) {
const { open } = cardPaymentModal; const { visible } = cardPaymentModal;
const { t } = useTranslation(); const { t } = useTranslation();
const handleCancel = () => { const handleCancel = () => {
@@ -35,7 +35,7 @@ function CardPaymentModalContainer({
return ( return (
<Modal <Modal
open={open} open={visible}
onOk={handleOK} onOk={handleOK}
onCancel={handleCancel} onCancel={handleCancel}
footer={[ footer={[

View File

@@ -4,11 +4,20 @@ import { Button, notification, Space } from "antd";
import axios from "axios"; import axios from "axios";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { messaging, requestForToken } from "../../firebase/firebase.utils"; import { messaging, requestForToken } from "../../firebase/firebase.utils";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FcmHandler from "../../utils/fcm-handler"; import FcmHandler from "../../utils/fcm-handler";
import ChatPopupComponent from "../chat-popup/chat-popup.component"; import ChatPopupComponent from "../chat-popup/chat-popup.component";
import "./chat-affix.styles.scss"; import "./chat-affix.styles.scss";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
});
export function ChatAffixContainer({ bodyshop, chatVisible }) { export function ChatAffixContainer({ bodyshop, chatVisible }) {
const { t } = useTranslation(); const { t } = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
@@ -19,7 +28,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
try { try {
const r = await axios.post("/notifications/subscribe", { const r = await axios.post("/notifications/subscribe", {
fcm_tokens: await getToken(messaging, { fcm_tokens: await getToken(messaging, {
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY, vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
}), }),
type: "messaging", type: "messaging",
imexshopid: bodyshop.imexshopid, imexshopid: bodyshop.imexshopid,
@@ -27,34 +36,35 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
console.log("FCM Topic Subscription", r.data); console.log("FCM Topic Subscription", r.data);
} catch (error) { } catch (error) {
console.log( console.log(
"Error attempting to subscribe to messaging topic: ", "Error attempting to subscribe to messaging topic: ",
error error
); );
notification.open({ notification.open({
type: "warning", type: "warning",
message: t("general.errors.fcm"), message: t("general.errors.fcm"),
btn: ( btn: (
<Space> <Space>
<Button <Button
onClick={async () => { onClick={async () => {
await requestForToken(); await requestForToken();
SubscribeToTopic();
}} SubscribeToTopic();
> }}
{t("general.actions.tryagain")} >
</Button> {t("general.actions.tryagain")}
<Button </Button>
onClick={() => { <Button
const win = window.open( onClick={() => {
"https://help.imex.online/en/article/enabling-notifications-o978xi/", const win = window.open(
"_blank" "https://help.imex.online/en/article/enabling-notifications-o978xi/",
); "_blank"
win.focus(); );
}} win.focus();
> }}
{t("general.labels.help")} >
</Button> {t("general.labels.help")}
</Space> </Button>
</Space>
), ),
}); });
} }
@@ -71,16 +81,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
payload: (payload && payload.data && payload.data.data) || payload.data, payload: (payload && payload.data && payload.data.data) || payload.data,
}); });
} }
let stopMessageListener, channel; let stopMessageListenr, channel;
try { try {
stopMessageListener = onMessage(messaging, handleMessage); stopMessageListenr = onMessage(messaging, handleMessage);
channel = new BroadcastChannel("imex-sw-messages"); channel = new BroadcastChannel("imex-sw-messages");
channel.addEventListener("message", handleMessage); channel.addEventListener("message", handleMessage);
} catch (error) { } catch (error) {
console.log("Unable to set event listeners."); console.log("Unable to set event listeners.");
} }
return () => { return () => {
stopMessageListener && stopMessageListener(); stopMessageListenr && stopMessageListenr();
channel && channel.removeEventListener("message", handleMessage); channel && channel.removeEventListener("message", handleMessage);
}; };
}, [client]); }, [client]);
@@ -88,10 +98,9 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>; if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return ( return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}> <div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null} {bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
</div> </div>
); );
} }
export default connect(mapStateToProps, null)(ChatAffixContainer);
export default ChatAffixContainer;

View File

@@ -1,122 +1,120 @@
import {Badge, Card, List, Space, 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 {
import {createStructuredSelector} from "reselect"; AutoSizer,
import {setSelectedConversation} from "../../redux/messaging/messaging.actions"; CellMeasurer,
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors"; CellMeasurerCache,
import {TimeAgoFormatter} from "../../utils/DateFormatter"; List as VirtualizedList,
} from "react-virtualized";
import { createStructuredSelector } from "reselect";
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import _ from "lodash";
import "./chat-conversation-list.styles.scss"; import "./chat-conversation-list.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
selectedConversation: selectSelectedConversation, selectedConversation: selectSelectedConversation,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setSelectedConversation: (conversationId) => setSelectedConversation: (conversationId) =>
dispatch(setSelectedConversation(conversationId)), dispatch(setSelectedConversation(conversationId)),
}); });
function ChatConversationListComponent({ function ChatConversationListComponent({
conversationList, conversationList,
selectedConversation, selectedConversation,
setSelectedConversation, setSelectedConversation,
loadMoreConversations, loadMoreConversations,
}) { }) {
const cache = new CellMeasurerCache({ const cache = new CellMeasurerCache({
fixedWidth: true, fixedWidth: true,
defaultHeight: 60, defaultHeight: 60,
}); });
const rowRenderer = ({index, key, style, parent}) => { const rowRenderer = ({ index, key, style, parent }) => {
const item = conversationList[index]; const item = conversationList[index];
const cardContentRight =
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
const cardContentLeft = item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx}>{j.job.ro_number}</Tag>
))
: null;
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
OwnerNameDisplayFunction(j.job)
))}</>
const cardTitle = <>
{item.label && <Tag color="blue">{item.label}</Tag>}
{item.job_conversations.length > 0 ? (
<Space direction="vertical">
{names}
</Space>
) : (
<Space>
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
</Space>
)}
</>
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
const getCardStyle = () =>
item.id === selectedConversation
? { backgroundColor: 'rgba(128, 128, 128, 0.2)' }
: { backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff' };
return (
<CellMeasurer
key={key}
cache={cache}
parent={parent}
columnIndex={0}
rowIndex={index}
>
<List.Item
onClick={() => setSelectedConversation(item.id)}
style={style}
className={`chat-list-item
${
item.id === selectedConversation
? "chat-list-selected-conversation"
: null
}`}
>
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}>
{cardContentLeft}
</div>
<div
style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
</Card>
</List.Item>
</CellMeasurer>
);
};
return ( return (
<div className="chat-list-container"> <CellMeasurer
<AutoSizer> key={key}
{({height, width}) => ( cache={cache}
<VirtualizedList parent={parent}
height={height} columnIndex={0}
width={width} rowIndex={index}
rowCount={conversationList.length} >
rowHeight={cache.rowHeight} <List.Item
rowRenderer={rowRenderer} onClick={() => setSelectedConversation(item.id)}
onScroll={({scrollTop, scrollHeight, clientHeight}) => { className={`chat-list-item ${
if (scrollTop + clientHeight === scrollHeight) { item.id === selectedConversation
loadMoreConversations(); ? "chat-list-selected-conversation"
} : null
}} }`}
/> style={style}
)} >
</AutoSizer> <div
</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 (
<div className="chat-list-container">
<AutoSizer>
{({ height, width }) => (
<VirtualizedList
height={height}
width={width}
rowCount={conversationList.length}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
if (scrollTop + clientHeight === scrollHeight) {
loadMoreConversations();
}
}}
/>
)}
</AutoSizer>
</div>
);
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ChatConversationListComponent); )(ChatConversationListComponent);

View File

@@ -1,14 +1,27 @@
.chat-list-selected-conversation {
background-color: rgba(128, 128, 128, 0.2);
}
.chat-list-container { .chat-list-container {
flex: 1;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
border: 1px solid gainsboro; border: 1px solid gainsboro;
} }
.chat-list-item { .chat-list-item {
.ant-card-head { display: flex;
border: none; flex-direction: row;
}
&:hover { &:hover {
cursor: pointer; cursor: pointer;
color: #ff7a00; color: #ff7a00;
} }
.chat-name {
flex: 1;
display: inline;
}
.ro-number-tag {
align-self: baseline;
}
padding: 12px 24px;
border-bottom: 1px solid gainsboro;
} }

View File

@@ -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) || []

View File

@@ -27,7 +27,7 @@ export function ChatMediaSelector({
conversation, conversation,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, { const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only", fetchPolicy: "network-only",
@@ -39,13 +39,13 @@ export function ChatMediaSelector({
}, },
skip: skip:
!open || !visible ||
!conversation.job_conversations || !conversation.job_conversations ||
conversation.job_conversations.length === 0, conversation.job_conversations.length === 0,
}); });
const handleVisibleChange = (change) => { const handleVisibleChange = (visible) => {
setOpen(change); setVisible(visible);
}; };
useEffect(() => { useEffect(() => {
@@ -65,7 +65,7 @@ export function ChatMediaSelector({
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
/> />
)} )}
{bodyshop.uselocalmediaserver && open && ( {bodyshop.uselocalmediaserver && visible && (
<JobDocumentsLocalGalleryExternal <JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={ jobId={
@@ -88,8 +88,8 @@ export function ChatMediaSelector({
} }
title={t("messaging.labels.selectmedia")} title={t("messaging.labels.selectmedia")}
trigger="click" trigger="click"
open={open} visible={visible}
onOpenChange={handleVisibleChange} onVisibleChange={handleVisibleChange}
> >
<Badge count={selectedMedia.filter((s) => s.isSelected).length}> <Badge count={selectedMedia.filter((s) => s.isSelected).length}>
<PictureFilled style={{ margin: "0 .5rem" }} /> <PictureFilled style={{ margin: "0 .5rem" }} />

View File

@@ -1,7 +1,7 @@
import Icon from "@ant-design/icons"; import Icon from "@ant-design/icons";
import { Tooltip } from "antd"; import { Tooltip } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import dayjs from "../../utils/day"; import moment from "moment";
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { MdDone, MdDoneAll } from "react-icons/md"; import { MdDone, MdDoneAll } from "react-icons/md";
import { import {
@@ -52,7 +52,7 @@ export default function ChatMessageListComponent({ messages }) {
<div style={{ fontSize: 10 }}> <div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", { {i18n.t("messaging.labels.sentby", {
by: messages[index].userid, by: messages[index].userid,
time: dayjs(messages[index].created_at).format( time: moment(messages[index].created_at).format(
"MM/DD/YYYY @ hh:mm a" "MM/DD/YYYY @ hh:mm a"
), ),
})} })}

View File

@@ -1,5 +1,5 @@
import { PlusCircleOutlined } from "@ant-design/icons"; import { PlusCircleOutlined } from "@ant-design/icons";
import { Dropdown } from "antd"; import { Dropdown, Menu } from "antd";
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";
@@ -16,16 +16,19 @@ const mapDispatchToProps = (dispatch) => ({
}); });
export function ChatPresetsComponent({ bodyshop, setMessage, className }) { export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const menu = (
const items = bodyshop.md_messaging_presets.map((i, idx) => ({ <Menu>
key: idx, {bodyshop.md_messaging_presets.map((i, idx) => (
label: (i.label), <Menu.Item onClick={() => setMessage(i.text)} key={idx}>
onClick: () => setMessage(i.text), {i.label}
})); </Menu.Item>
))}
</Menu>
);
return ( return (
<div className={className}> <div className={className}>
<Dropdown trigger={["click"]} menu={{items}}> <Dropdown trigger={["click"]} overlay={menu}>
<PlusCircleOutlined /> <PlusCircleOutlined />
</Dropdown> </Dropdown>
</div> </div>

View File

@@ -1,45 +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);
const generateDocument = (type) => {
setLoading(true);
GenerateDocument(
{
name: TemplateList("messaging").conversation_list.key,
variables: { id: conversation.id },
},
{
subject: TemplateList("messaging").conversation_list.subject,
},
type,
conversation.id
).catch(e => {
console.warn('Something went wrong generating a document.');
});
setLoading(false);
}
return (
<Space wrap>
<PrinterOutlined onClick={() => generateDocument('p')}/>
<MailOutlined onClick={() => generateDocument('e')}/>
{loading && <Spin />}
</Space>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);

View File

@@ -9,17 +9,17 @@ export default function ChatTagRoComponent({
loading, loading,
handleSearch, handleSearch,
handleInsertTag, handleInsertTag,
setOpen, setVisible,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Space> <Space flex>
<div style={{ width: "15rem" }}> <div style={{ width: "15rem" }}>
<Select <Select
showSearch showSearch
autoFocus autoFocus
popupMatchSelectWidth dropdownMatchSelectWidth
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
filterOption={false} filterOption={false}
onSearch={handleSearch} onSearch={handleSearch}
@@ -38,7 +38,7 @@ export default function ChatTagRoComponent({
{loading ? ( {loading ? (
<LoadingOutlined /> <LoadingOutlined />
) : ( ) : (
<CloseCircleOutlined onClick={() => setOpen(false)} /> <CloseCircleOutlined onClick={() => setVisible(false)} />
)} )}
</Space> </Space>
); );

View File

@@ -11,7 +11,7 @@ import ChatTagRo from "./chat-tag-ro.component";
export default function ChatTagRoContainer({ conversation }) { export default function ChatTagRoContainer({ conversation }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS); const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
@@ -33,7 +33,7 @@ export default function ChatTagRoContainer({ conversation }) {
const handleInsertTag = (value, option) => { const handleInsertTag = (value, option) => {
logImEXEvent("messaging_add_job_tag"); logImEXEvent("messaging_add_job_tag");
insertTag({ variables: { jobId: option.key } }); insertTag({ variables: { jobId: option.key } });
setOpen(false); setVisible(false);
}; };
const existingJobTags = const existingJobTags =
@@ -47,16 +47,16 @@ export default function ChatTagRoContainer({ conversation }) {
return ( return (
<div> <div>
{open ? ( {visible ? (
<ChatTagRo <ChatTagRo
loading={loading} loading={loading}
roOptions={roOptions} roOptions={roOptions}
handleSearch={handleSearch} handleSearch={handleSearch}
handleInsertTag={handleInsertTag} handleInsertTag={handleInsertTag}
setOpen={setOpen} setVisible={setVisible}
/> />
) : ( ) : (
<Tag onClick={() => setOpen(true)}> <Tag onClick={() => setVisible(true)}>
<PlusOutlined /> <PlusOutlined />
{t("messaging.actions.link")} {t("messaging.actions.link")}
</Tag> </Tag>

View File

@@ -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",

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import dayjs from "../../utils/day"; import moment from "moment";
import React from "react"; import React from "react";
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries"; import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
@@ -7,7 +7,7 @@ import ContractCarsComponent from "./contract-cars.component";
export default function ContractCarsContainer({ selectedCarState, form }) { export default function ContractCarsContainer({ selectedCarState, form }) {
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, { const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
variables: { today: dayjs().format("YYYY-MM-DD") }, variables: { today: moment().format("YYYY-MM-DD") },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
}); });

View File

@@ -10,11 +10,11 @@ import {
Space, Space,
} from "antd"; } from "antd";
import axios from "axios"; import axios from "axios";
import dayjs from "../../utils/day"; 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 { connect } from "react-redux"; import { connect } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries"; import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
import { import {
@@ -38,17 +38,17 @@ export function ContractConvertToRo({
disabled, disabled,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [insertJob] = useMutation(INSERT_NEW_JOB); const [insertJob] = useMutation(INSERT_NEW_JOB);
const history = useNavigate(); const history = useHistory();
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
const contractLength = dayjs(contract.actualreturn).diff( const contractLength = moment(contract.actualreturn).diff(
dayjs(contract.start), moment(contract.start),
"day" "days"
); );
const billingLines = []; const billingLines = [];
if (contractLength > 0) if (contractLength > 0)
@@ -306,7 +306,7 @@ export function ContractConvertToRo({
}); });
} }
setOpen(false); setVisible(false);
setLoading(false); setLoading(false);
}; };
@@ -380,7 +380,7 @@ export function ContractConvertToRo({
<Button type="primary" htmlType="submit" loading={loading}> <Button type="primary" htmlType="submit" loading={loading}>
{t("contracts.actions.convertoro")} {t("contracts.actions.convertoro")}
</Button> </Button>
<Button onClick={() => setOpen(false)}> <Button onClick={() => setVisible(false)}>
{t("general.actions.close")} {t("general.actions.close")}
</Button> </Button>
</Space> </Space>
@@ -390,9 +390,9 @@ export function ContractConvertToRo({
return ( return (
<div> <div>
<Popover content={popContent} open={open}> <Popover content={popContent} visible={visible}>
<Button <Button
onClick={() => setOpen(true)} onClick={() => setVisible(true)}
loading={loading} loading={loading}
disabled={!contract.dailyrate || !contract.actualreturn || disabled} disabled={!contract.dailyrate || !contract.actualreturn || disabled}
> >

View File

@@ -1,6 +1,6 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { Form, Input, InputNumber, Space } from "antd"; import { Form, Input, InputNumber, Space } from "antd";
import dayjs from "../../utils/day"; 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";
@@ -96,8 +96,8 @@ export default function ContractFormComponent({
const dueForService = const dueForService =
selectedCar && selectedCar &&
selectedCar.nextservicedate && selectedCar.nextservicedate &&
dayjs(selectedCar.nextservicedate).isBefore( moment(selectedCar.nextservicedate).isBefore(
dayjs(form.getFieldValue("scheduledreturn")) moment(form.getFieldValue("scheduledreturn"))
); );
if (mileageOver || dueForService) if (mileageOver || dueForService)
@@ -190,9 +190,9 @@ export default function ContractFormComponent({
} }
> >
{() => { {() => {
const dlExpiresBeforeReturn = dayjs( const dlExpiresBeforeReturn = moment(
form.getFieldValue("driver_dlexpiry") form.getFieldValue("driver_dlexpiry")
).isBefore(dayjs(form.getFieldValue("scheduledreturn"))); ).isBefore(moment(form.getFieldValue("scheduledreturn")));
return ( return (
<div> <div>

View File

@@ -1,5 +1,5 @@
import { Button, Input, Modal, Typography } from "antd"; import { Button, Input, Modal, Typography } from "antd";
import dayjs from "../../utils/day"; 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 aamva from "../../utils/aamva"; import aamva from "../../utils/aamva";
@@ -26,8 +26,8 @@ export default function ContractLicenseDecodeButton({ form }) {
const values = { const values = {
driver_dlnumber: decodedBarcode.dl, driver_dlnumber: decodedBarcode.dl,
driver_dlexpiry: dayjs( driver_dlexpiry: moment(
`20${decodedBarcode.expiration_date}${dayjs( `20${decodedBarcode.expiration_date}${moment(
decodedBarcode.birthday decodedBarcode.birthday
).format("DD")}` ).format("DD")}`
), ),
@@ -38,7 +38,7 @@ export default function ContractLicenseDecodeButton({ form }) {
driver_city: decodedBarcode.city, driver_city: decodedBarcode.city,
driver_state: decodedBarcode.state, driver_state: decodedBarcode.state,
driver_zip: decodedBarcode.postal_code, driver_zip: decodedBarcode.postal_code,
driver_dob: dayjs(decodedBarcode.birthday), driver_dob: moment(decodedBarcode.birthday),
}; };
form.setFieldsValue(values); form.setFieldsValue(values);
@@ -55,7 +55,7 @@ export default function ContractLicenseDecodeButton({ form }) {
return ( return (
<div> <div>
<Modal <Modal
open={modalVisible} visible={modalVisible}
okText={t("contracts.actions.senddltoform")} okText={t("contracts.actions.senddltoform")}
onOk={handleInsertForm} onOk={handleInsertForm}
okButtonProps={{ disabled: !!!decodedBarcode }} okButtonProps={{ disabled: !!!decodedBarcode }}
@@ -94,14 +94,14 @@ export default function ContractLicenseDecodeButton({ form }) {
{decodedBarcode.address} {decodedBarcode.address}
</DataLabel> </DataLabel>
<DataLabel label={t("contracts.fields.driver_dlexpiry")}> <DataLabel label={t("contracts.fields.driver_dlexpiry")}>
{dayjs( {moment(
`20${decodedBarcode.expiration_date}${dayjs( `20${decodedBarcode.expiration_date}${moment(
decodedBarcode.birthday decodedBarcode.birthday
).format("DD")}` ).format("DD")}`
).format("MM/DD/YYYY")} ).format("MM/DD/YYYY")}
</DataLabel> </DataLabel>
<DataLabel label={t("contracts.fields.driver_dob")}> <DataLabel label={t("contracts.fields.driver_dob")}>
{dayjs(decodedBarcode.birthday).format("MM/DD/YYYY")} {moment(decodedBarcode.birthday).format("MM/DD/YYYY")}
</DataLabel> </DataLabel>
<div> <div>
<Typography.Title level={4}> <Typography.Title level={4}>

View File

@@ -31,7 +31,7 @@ export function ContractsFindModalContainer({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { open } = contractFinderModal; const { visible } = contractFinderModal;
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -52,14 +52,14 @@ export function ContractsFindModalContainer({
}; };
useEffect(() => { useEffect(() => {
if (open) { if (visible) {
form.resetFields(); form.resetFields();
} }
}, [open, form]); }, [visible, form]);
return ( return (
<Modal <Modal
open={open} visible={visible}
width="70%" width="70%"
title={t("contracts.labels.findermodal")} title={t("contracts.labels.findermodal")}
onCancel={() => toggleModalVisible()} onCancel={() => toggleModalVisible()}

View File

@@ -3,13 +3,13 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
import queryString from "query-string"; 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, useNavigate, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions"; 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 dayjs from "../../utils/day"; import moment from "moment";
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";
@@ -39,7 +39,7 @@ export function ContractsList({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" }, filteredInfo: { text: "" },
}); });
const history = useNavigate(); const history = useHistory();
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { page } = search; const { page } = search;
@@ -152,8 +152,8 @@ export function ContractsList({
render: (text, record) => render: (text, record) =>
(record.actualreturn && (record.actualreturn &&
record.start && record.start &&
`${dayjs(record.actualreturn) `${moment(record.actualreturn)
.diff(dayjs(record.start), "day", true) .diff(moment(record.start), "days", true)
.toFixed(1)} days`) || .toFixed(1)} days`) ||
"", "",
}, },
@@ -164,7 +164,7 @@ export function ContractsList({
search.page = pagination.current; search.page = pagination.current;
search.sortcolumn = sorter.columnKey; search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order; search.sortorder = sorter.order;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
return ( return (
@@ -179,7 +179,7 @@ export function ContractsList({
<Button <Button
onClick={() => { onClick={() => {
delete search.search; delete search.search;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
> >
{t("general.actions.clear")} {t("general.actions.clear")}
@@ -196,7 +196,7 @@ export function ContractsList({
placeholder={search.searh || t("general.labels.search")} placeholder={search.searh || t("general.labels.search")}
onSearch={(value) => { onSearch={(value) => {
search.search = value; search.search = value;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}} }}
/> />
</Space> </Space>

View File

@@ -1,5 +1,5 @@
import { DownOutlined } from "@ant-design/icons"; import { DownOutlined } from "@ant-design/icons";
import { Dropdown } from "antd"; import { Dropdown, Menu } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
@@ -18,16 +18,20 @@ export function ContractsRatesChangeButton({ disabled, form, bodyshop }) {
form.setFieldsValue(rate); form.setFieldsValue(rate);
}; };
const menuItems = bodyshop.md_ccc_rates.map((i, idx) => ({ const menu = (
key: idx, <div>
label: i.label, <Menu onClick={handleClick}>
value: i, {bodyshop.md_ccc_rates.map((rate, idx) => (
})); <Menu.Item value={rate} key={idx}>
{rate.label}
const menu = {items: menuItems, onClick: handleClick}; </Menu.Item>
))}
</Menu>
</div>
);
return ( return (
<Dropdown menu={menu} disabled={disabled}> <Dropdown overlay={menu} disabled={disabled}>
<a <a
className="ant-dropdown-link" className="ant-dropdown-link"
href=" #" href=" #"

View File

@@ -2,7 +2,7 @@ import { Card, Table } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useLocation, useNavigate } 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"; import {pageLimit} from "../../utils/config";
@@ -11,9 +11,9 @@ export default function CourtesyCarContractListComponent({
contracts, contracts,
totalContracts, totalContracts,
}) { }) {
const search =queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder } = search; const { page, sortcolumn, sortorder } = search;
const history = useNavigate(); const history = useHistory();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -81,7 +81,7 @@ export default function CourtesyCarContractListComponent({
search.page = pagination.current; search.page = pagination.current;
search.sortcolumn = sorter.columnKey; search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order; search.sortorder = sorter.order;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
return ( return (

View File

@@ -1,14 +1,12 @@
import { WarningFilled } from "@ant-design/icons"; import { WarningFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { Button, Form, Input, InputNumber, Space } from "antd"; import { Button, Form, Input, InputNumber, PageHeader, Space } from "antd";
import {PageHeader} from "@ant-design/pro-layout"; import moment from "moment";
import dayjs from "../../utils/day";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; 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";
@@ -215,9 +213,6 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
> >
<CourtesyCarStatus /> <CourtesyCarStatus />
</Form.Item> </Form.Item>
<Form.Item label={t("courtesycars.fields.readiness")} name="readiness">
<CourtesyCarReadiness />
</Form.Item>
<div> <div>
<Form.Item <Form.Item
label={t("courtesycars.fields.nextservicekm")} label={t("courtesycars.fields.nextservicekm")}
@@ -232,9 +227,8 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
> >
{() => { {() => {
const nextservicekm = form.getFieldValue("nextservicekm"); const nextservicekm = form.getFieldValue("nextservicekm");
const mileageOver = nextservicekm const mileageOver =
? nextservicekm <= form.getFieldValue("mileage") nextservicekm && nextservicekm <= form.getFieldValue("mileage");
: false;
if (mileageOver) if (mileageOver)
return ( return (
<Space direction="vertical" style={{ color: "tomato" }}> <Space direction="vertical" style={{ color: "tomato" }}>
@@ -264,7 +258,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const nextservicedate = form.getFieldValue("nextservicedate"); const nextservicedate = form.getFieldValue("nextservicedate");
const dueForService = const dueForService =
nextservicedate && nextservicedate &&
dayjs(nextservicedate).endOf("day").isSameOrBefore(dayjs()); moment(nextservicedate).endOf("day").isSameOrBefore(moment());
if (dueForService) if (dueForService)
return ( return (
@@ -305,7 +299,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const expires = form.getFieldValue("registrationexpires"); const expires = form.getFieldValue("registrationexpires");
const dateover = const dateover =
expires && dayjs(expires).endOf("day").isBefore(dayjs()); expires && moment(expires).endOf("day").isBefore(moment());
if (dateover) if (dateover)
return ( return (
@@ -341,7 +335,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
const expires = form.getFieldValue("insuranceexpires"); const expires = form.getFieldValue("insuranceexpires");
const dateover = const dateover =
expires && dayjs(expires).endOf("day").isBefore(dayjs()); expires && moment(expires).endOf("day").isBefore(moment());
if (dateover) if (dateover)
return ( return (

View File

@@ -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;
}
},
}}
/> />
); );
}; };

View File

@@ -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);

View File

@@ -7,7 +7,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors"; import { selectCourtesyCarReturn } from "../../redux/modals/modals.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component"; import CourtesyCarReturnModalComponent from "./courtesy-car-return-modal.component";
import dayjs from "../../utils/day"; import moment from "moment";
import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries"; import { RETURN_CONTRACT } from "../../graphql/cccontracts.queries";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
@@ -26,7 +26,7 @@ export function CCReturnModalContainer({
bodyshop, bodyshop,
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { open, context, actions } = courtesyCarReturnModal; const { visible, context, actions } = courtesyCarReturnModal;
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [updateContract] = useMutation(RETURN_CONTRACT); const [updateContract] = useMutation(RETURN_CONTRACT);
@@ -64,7 +64,7 @@ export function CCReturnModalContainer({
return ( return (
<Modal <Modal
title={t("courtesycars.labels.return")} title={t("courtesycars.labels.return")}
open={open} visible={visible}
onCancel={() => toggleModalVisible()} onCancel={() => toggleModalVisible()}
width={"90%"} width={"90%"}
okText={t("general.actions.save")} okText={t("general.actions.save")}
@@ -74,7 +74,7 @@ export function CCReturnModalContainer({
<Form <Form
form={form} form={form}
onFinish={handleFinish} onFinish={handleFinish}
initialValues={{ fuel: 100, actualreturn: dayjs(new Date()) }} initialValues={{ fuel: 100, actualreturn: moment(new Date()) }}
> >
<CourtesyCarReturnModalComponent /> <CourtesyCarReturnModalComponent />
</Form> </Form>

View File

@@ -4,11 +4,12 @@ import {
Card, Card,
Dropdown, Dropdown,
Input, Input,
Menu,
Space, Space,
Table, Table,
Tooltip, Tooltip,
} from "antd"; } from "antd";
import dayjs from "../../utils/day"; 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";
@@ -73,10 +74,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
render: (text, record) => { render: (text, record) => {
const { nextservicedate, nextservicekm, mileage } = record; const { nextservicedate, nextservicekm, mileage } = record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false; const mileageOver = nextservicekm <= mileage;
const dueForService = const dueForService =
nextservicedate && dayjs(nextservicedate).endOf('day').isSameOrBefore(dayjs()); nextservicedate && moment(nextservicedate).isBefore(moment());
return ( return (
<Space> <Space>
@@ -90,26 +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),
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",
@@ -150,36 +131,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) => alphaSort(a.fuel, b.fuel),
sortOrder:
state.sortedInfo.columnKey === "fuel" && state.sortedInfo.order,
render: (text, record) => {
switch (record.fuel) {
case 100:
return t("courtesycars.labels.fuel.full");
case 88:
return t("courtesycars.labels.fuel.78");
case 63:
return t("courtesycars.labels.fuel.58");
case 50:
return t("courtesycars.labels.fuel.12");
case 38:
return t("courtesycars.labels.fuel.34");
case 25:
return t("courtesycars.labels.fuel.14");
case 13:
return t("courtesycars.labels.fuel.18");
case 0:
return t("courtesycars.labels.fuel.empty");
default:
return record.fuel;
}
},
},
{ {
title: t("courtesycars.labels.outwith"), title: t("courtesycars.labels.outwith"),
dataIndex: "outwith", dataIndex: "outwith",
@@ -227,27 +178,6 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
(t(c.status) || "").toLowerCase().includes(searchText.toLowerCase()) (t(c.status) || "").toLowerCase().includes(searchText.toLowerCase())
) )
: courtesycars; : courtesycars;
const items = [
{
key: "courtesycar_inventory",
label: t("printcenter.courtesycarcontract.courtesy_car_inventory"),
onClick: () =>
GenerateDocument(
{
name: TemplateList("courtesycar").courtesy_car_inventory.key,
variables: {
//id: contract.id
},
},
{},
"p"
),
},
];
const menu = { items };
return ( return (
<Card <Card
title={t("menus.header.courtesycars")} title={t("menus.header.courtesycars")}
@@ -256,7 +186,30 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<Dropdown trigger="click" menu={menu}> <Dropdown
trigger="click"
overlay={
<Menu>
<Menu.Item
onClick={() =>
GenerateDocument(
{
name: TemplateList("courtesycar").courtesy_car_inventory
.key,
variables: {
//id: contract.id
},
},
{},
"p"
)
}
>
{t("printcenter.courtesycarcontract.courtesy_car_inventory")}
</Menu.Item>
</Menu>
}
>
<Button>{t("general.labels.print")}</Button> <Button>{t("general.labels.print")}</Button>
</Dropdown> </Dropdown>
<Link to={`/manage/courtesycars/new`}> <Link to={`/manage/courtesycars/new`}>

View File

@@ -3,7 +3,7 @@ import { Button, Card, Table } from "antd";
import queryString from "query-string"; 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, useNavigate, 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 OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
@@ -17,7 +17,7 @@ export default function CsiResponseListPaginated({
}) { }) {
const search = queryString.parse(useLocation().search); const search = queryString.parse(useLocation().search);
const { responseid, page, sortcolumn, sortorder } = search; const { responseid, page, sortcolumn, sortorder } = search;
const history = useNavigate(); const history = useHistory();
const [state, setState] = useState({ const [state, setState] = useState({
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" }, filteredInfo: { text: "" },
@@ -80,18 +80,18 @@ export default function CsiResponseListPaginated({
search.page = pagination.current; search.page = pagination.current;
search.sortcolumn = sorter.columnKey; search.sortcolumn = sorter.columnKey;
search.sortorder = sorter.order; search.sortorder = sorter.order;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
}; };
const handleOnRowClick = (record) => { const handleOnRowClick = (record) => {
if (record) { if (record) {
if (record.id) { if (record.id) {
search.responseid = record.id; search.responseid = record.id;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
} }
} else { } else {
delete search.responseid; delete search.responseid;
history({ search: queryString.stringify(search) }); history.push({ search: queryString.stringify(search) });
} }
}; };

View File

@@ -1,6 +1,6 @@
import { Card } from "antd"; import { Card } from "antd";
import _ from "lodash"; import _ from "lodash";
import dayjs from "../../../utils/day"; import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -27,7 +27,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) => const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
dayjs(item.date).format("YYYY-MM-DD") moment(item.date).format("YYYY-MM-DD")
); );
const listOfDays = Utils.ListOfDaysInCurrentMonth(); const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -53,7 +53,7 @@ export default function DashboardMonthlyEmployeeEfficiency({
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100; ((dailyHrs.productive - dailyHrs.actual) / dailyHrs.actual + 1) * 100;
const theValue = { const theValue = {
date: dayjs(val).format("DD"), date: moment(val).format("DD"),
// ...dailyHrs, // ...dailyHrs,
actual: dailyHrs.actual.toFixed(1), actual: dailyHrs.actual.toFixed(1),
productive: dailyHrs.productive.toFixed(1), productive: dailyHrs.productive.toFixed(1),
@@ -159,9 +159,9 @@ export default function DashboardMonthlyEmployeeEfficiency({
} }
export const DashboardMonthlyEmployeeEfficiencyGql = ` export const DashboardMonthlyEmployeeEfficiencyGql = `
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${dayjs() monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${moment()
.startOf("month") .startOf("month")
.format("YYYY-MM-DD")}"}},{date: {_lte: "${dayjs() .format("YYYY-MM-DD")}"}},{date: {_lte: "${moment()
.endOf("month") .endOf("month")
.format("YYYY-MM-DD")}"}} ]}) { .format("YYYY-MM-DD")}"}} ]}) {
actualhrs actualhrs

View File

@@ -21,8 +21,6 @@ export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
async function getCostingData() { async function getCostingData() {
if (data && data.monthly_sales) { if (data && data.monthly_sales) {
setLoading(true); setLoading(true);
console.log('defaults:')
console.dir(axios.defaults);
const response = await axios.post("/job/costingmulti", { const response = await axios.post("/job/costingmulti", {
jobids: data.monthly_sales.map((x) => x.id), jobids: data.monthly_sales.map((x) => x.id),
}); });

View File

@@ -1,5 +1,5 @@
import { Card } from "antd"; import { Card } from "antd";
import dayjs from "../../../utils/day"; import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import _ from "lodash"; import _ from "lodash";
@@ -24,7 +24,7 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />; if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const jobsByDate = _.groupBy(data.monthly_sales, (item) => const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
dayjs(item.date_invoiced).format("YYYY-MM-DD") moment(item.date_invoiced).format("YYYY-MM-DD")
); );
const listOfDays = Utils.ListOfDaysInCurrentMonth(); const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -43,7 +43,7 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
} }
const theValue = { const theValue = {
date: dayjs(val).format("DD"), date: moment(val).format("DD"),
dailySales: dailySales.getAmount() / 100, dailySales: dailySales.getAmount() / 100,
accSales: accSales:
acc.length > 0 acc.length > 0

View File

@@ -1,6 +1,6 @@
import { Card, Statistic } from "antd"; import { Card, Statistic } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import dayjs from "../../../utils/day"; import moment from "moment";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component"; import DashboardRefreshRequired from "../refresh-required.component";
@@ -36,10 +36,10 @@ export const DashboardProjectedMonthlySalesGql = `
_or: [ _or: [
{_and: [ {_and: [
{date_invoiced:{_is_null: false }}, {date_invoiced:{_is_null: false }},
{date_invoiced: {_gte: "${dayjs() {date_invoiced: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs() .toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}}]}, .toISOString()}"}}]},
@@ -47,10 +47,10 @@ export const DashboardProjectedMonthlySalesGql = `
_and:[ _and:[
{date_invoiced:{_is_null: true }}, {date_invoiced:{_is_null: true }},
{actual_completion: {_gte: "${dayjs() {actual_completion: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {actual_completion: {_lte: "${dayjs() .toISOString()}"}}, {actual_completion: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}} .toISOString()}"}}
@@ -61,10 +61,10 @@ _and:[
{_and: [ {_and: [
{date_invoiced: {_is_null: true}}, {date_invoiced: {_is_null: true}},
{actual_completion: {_is_null: true}} {actual_completion: {_is_null: true}}
{scheduled_completion: {_gte: "${dayjs() {scheduled_completion: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {scheduled_completion: {_lte: "${dayjs() .toISOString()}"}}, {scheduled_completion: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}} .toISOString()}"}}

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd"; import { Card, Space, Table, Tooltip } from "antd";
import dayjs from "../../../utils/day"; 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";
@@ -49,14 +49,14 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
v_vin: item.job.v_vin, v_vin: item.job.v_vin,
vehicleid: item.job.vehicleid, vehicleid: item.job.vehicleid,
note: item.note, note: item.note,
start: dayjs(item.start).format("hh:mm a"), start: moment(item.start).format("hh:mm a"),
title: item.title, title: item.title,
}; };
appt.push(i); appt.push(i);
} }
}); });
appt.sort(function (a, b) { appt.sort(function (a, b) {
return new dayjs(a.start) - new dayjs(b.start); return new moment(a.start) - new moment(b.start);
}); });
const columns = [ const columns = [
@@ -189,7 +189,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledintoday", { title={t("dashboard.titles.scheduledintoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
{...cardProps} {...cardProps}
> >
@@ -209,9 +209,9 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
} }
export const DashboardScheduledInTodayGql = ` export const DashboardScheduledInTodayGql = `
scheduled_in_today: appointments(where: {start: {_gte: "${dayjs() scheduled_in_today: appointments(where: {start: {_gte: "${moment()
.startOf("day") .startOf("day")
.toISOString()}", _lte: "${dayjs() .toISOString()}", _lte: "${moment()
.endOf("day") .endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) { .toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
canceled canceled

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Card, Space, Table, Tooltip } from "antd"; import { Card, Space, Table, Tooltip } from "antd";
import dayjs from "../../../utils/day"; 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";
@@ -22,13 +22,12 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
if (!data.scheduled_out_today) if (!data.scheduled_out_today)
return <DashboardRefreshRequired {...cardProps} />; return <DashboardRefreshRequired {...cardProps} />;
const filteredScheduledOutToday = data.scheduled_out_today.map((item) => { data.scheduled_out_today.forEach((item) => {
return { item.scheduled_completion= moment(item.scheduled_completion).format("hh:mm a")
...item, });
scheduled_completion: dayjs(item.scheduled_completion).format("hh:mm a"), data.scheduled_out_today.sort(function (a, b) {
timestamp: dayjs(item.scheduled_completion).valueOf(), return new Date(a.scheduled_completion) - new Date(b.scheduled_completion);
} });
}).sort((a, b) => a.timestamp - b.timestamp);
const columns = [ const columns = [
{ {
@@ -160,7 +159,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
return ( return (
<Card <Card
title={t("dashboard.titles.scheduledouttoday", { title={t("dashboard.titles.scheduledouttoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
})} })}
{...cardProps} {...cardProps}
> >
@@ -172,7 +171,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
scroll={{ x: true, y: "calc(100% - 2em)" }} scroll={{ x: true, y: "calc(100% - 2em)" }}
rowKey="id" rowKey="id"
style={{ height: "85%" }} style={{ height: "85%" }}
dataSource={filteredScheduledOutToday} dataSource={data.scheduled_out_today}
/> />
</div> </div>
</Card> </Card>
@@ -184,8 +183,8 @@ export const DashboardScheduledOutTodayGql = `
date_invoiced: {_is_null: true}, date_invoiced: {_is_null: true},
ro_number: {_is_null: false}, ro_number: {_is_null: false},
voided: {_eq: false}, voided: {_eq: false},
scheduled_completion: {_gte: "${dayjs().startOf("day").toISOString()}", scheduled_completion: {_gte: "${moment().startOf("day").toISOString()}",
_lte: "${dayjs().endOf("day").toISOString()}"}}) { _lte: "${moment().endOf("day").toISOString()}"}}) {
alt_transport alt_transport
clm_no clm_no
jobid: id jobid: id

View File

@@ -1,10 +1,9 @@
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, Space, notification } from "antd"; import { Button, Dropdown, Menu, PageHeader, Space, notification } from "antd";
import {PageHeader} from "@ant-design/pro-layout";
import i18next from "i18next"; import i18next from "i18next";
import _ from "lodash"; import _ from "lodash";
import dayjs from "../../utils/day"; 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";
@@ -123,15 +122,19 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
[data] [data]
); );
const existingLayoutKeys = state.items.map((i) => i.i); const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
const menuItems = Object.keys(componentList).map((key) => ({ <Menu onClick={handleAddComponent}>
key: key, {Object.keys(componentList).map((key) => (
label: componentList[key].label, <Menu.Item
value: key, key={key}
disabled: existingLayoutKeys.includes(key), value={key}
})); disabled={existingLayoutKeys.includes(key)}
>
const menu = {items: menuItems, onClick: handleAddComponent}; {componentList[key].label}
</Menu.Item>
))}
</Menu>
);
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
@@ -143,7 +146,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
<Dropdown menu={menu} trigger={["click"]}> <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button> <Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown> </Dropdown>
</Space> </Space>
@@ -273,7 +276,7 @@ const componentList = {
}, },
ScheduleInToday: { ScheduleInToday: {
label: i18next.t("dashboard.titles.scheduledintoday", { label: i18next.t("dashboard.titles.scheduledintoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
}), }),
component: DashboardScheduledInToday, component: DashboardScheduledInToday,
gqlFragment: DashboardScheduledInTodayGql, gqlFragment: DashboardScheduledInTodayGql,
@@ -284,7 +287,7 @@ const componentList = {
}, },
ScheduleOutToday: { ScheduleOutToday: {
label: i18next.t("dashboard.titles.scheduledouttoday", { label: i18next.t("dashboard.titles.scheduledouttoday", {
date: dayjs().startOf("day").format("MM/DD/YYYY"), date: moment().startOf("day").format("MM/DD/YYYY"),
}), }),
component: DashboardScheduledOutToday, component: DashboardScheduledOutToday,
gqlFragment: DashboardScheduledOutTodayGql, gqlFragment: DashboardScheduledOutTodayGql,
@@ -307,10 +310,10 @@ const createDashboardQuery = (state) => {
${componentBasedAdditions || ""} ${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [ monthly_sales: jobs(where: {_and: [
{ voided: {_eq: false}}, { voided: {_eq: false}},
{date_invoiced: {_gte: "${dayjs() {date_invoiced: {_gte: "${moment()
.startOf("month") .startOf("month")
.startOf("day") .startOf("day")
.toISOString()}"}}, {date_invoiced: {_lte: "${dayjs() .toISOString()}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month") .endOf("month")
.endOf("day") .endOf("day")
.toISOString()}"}}]}) { .toISOString()}"}}]}) {

View File

@@ -6,13 +6,13 @@ export default function DataLabel({
hideIfNull, hideIfNull,
children, children,
vertical, vertical,
open = true, visible = true,
valueStyle = {}, valueStyle = {},
valueClassName, valueClassName,
onValueClick, onValueClick,
...props ...props
}) { }) {
if (!open || (hideIfNull && !!!children)) return null; if (!visible || (hideIfNull && !!!children)) return null;
return ( return (
<div {...props} style={{ display: "flex" }}> <div {...props} style={{ display: "flex" }}>

View File

@@ -18,7 +18,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles); export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles);
export function DmsCdkVehicles({ bodyshop, form, socket, job }) { export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const [selectedModel, setSelectedModel] = useState(null); const [selectedModel, setSelectedModel] = useState(null);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -51,14 +51,14 @@ export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
<> <>
<Modal <Modal
width={"90%"} width={"90%"}
open={open} visible={visible}
onCancel={() => setOpen(false)} onCancel={() => setVisible(false)}
onOk={() => { onOk={() => {
form.setFieldsValue({ form.setFieldsValue({
dms_make: selectedModel.makecode, dms_make: selectedModel.makecode,
dms_model: selectedModel.modelcode, dms_model: selectedModel.modelcode,
}); });
setOpen(false); setVisible(false);
}} }}
> >
{error && <AlertComponent error={error.message} />} {error && <AlertComponent error={error.message} />}
@@ -90,7 +90,7 @@ export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
</Modal> </Modal>
<Button <Button
onClick={() => { onClick={() => {
setOpen(true); setVisible(true);
callSearch({ callSearch({
variables: { variables: {
search: job && job.v_model_desc && job.v_model_desc.substr(0, 3), search: job && job.v_model_desc && job.v_model_desc.substr(0, 3),

View File

@@ -21,29 +21,29 @@ export default connect(
export function DmsCustomerSelector({ bodyshop }) { export function DmsCustomerSelector({ bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [customerList, setcustomerList] = useState([]); const [customerList, setcustomerList] = useState([]);
const [open, setOpen] = useState(false); const [visible, setVisible] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null); const [selectedCustomer, setSelectedCustomer] = useState(null);
const [dmsType, setDmsType] = useState("cdk"); const [dmsType, setDmsType] = useState("cdk");
socket.on("cdk-select-customer", (customerList, callback) => { socket.on("cdk-select-customer", (customerList, callback) => {
setOpen(true); setVisible(true);
setDmsType("cdk"); setDmsType("cdk");
setcustomerList(customerList); setcustomerList(customerList);
}); });
socket.on("pbs-select-customer", (customerList, callback) => { socket.on("pbs-select-customer", (customerList, callback) => {
setOpen(true); setVisible(true);
setDmsType("pbs"); setDmsType("pbs");
setcustomerList(customerList); setcustomerList(customerList);
}); });
const onUseSelected = () => { const onUseSelected = () => {
setOpen(false); setVisible(false);
socket.emit(`${dmsType}-selected-customer`, selectedCustomer); socket.emit(`${dmsType}-selected-customer`, selectedCustomer);
setSelectedCustomer(null); setSelectedCustomer(null);
}; };
const onUseGeneric = () => { const onUseGeneric = () => {
setOpen(false); setVisible(false);
socket.emit( socket.emit(
`${dmsType}-selected-customer`, `${dmsType}-selected-customer`,
bodyshop.cdk_configuration.generic_customer_number bodyshop.cdk_configuration.generic_customer_number
@@ -52,7 +52,7 @@ export function DmsCustomerSelector({ bodyshop }) {
}; };
const onCreateNew = () => { const onCreateNew = () => {
setOpen(false); setVisible(false);
socket.emit(`${dmsType}-selected-customer`, null); socket.emit(`${dmsType}-selected-customer`, null);
setSelectedCustomer(null); setSelectedCustomer(null);
}; };
@@ -114,7 +114,7 @@ export function DmsCustomerSelector({ bodyshop }) {
}, },
]; ];
if (!open) return null; if (!visible) return null;
return ( return (
<Col span={24}> <Col span={24}>
<Table <Table

View File

@@ -1,5 +1,5 @@
import { Divider, Space, Tag, Timeline } from "antd"; import { Divider, Space, Tag, Timeline } from "antd";
import dayjs from "../../utils/day"; import moment from "moment";
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";
@@ -22,22 +22,20 @@ export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ socket, logs, bodyshop }) { export function DmsLogEvents({ socket, logs, bodyshop }) {
return ( return (
<Timeline <Timeline pending
pending reverse={true}
reverse={true} >
items={logs.map((log, idx) => ({ {logs.map((log, idx) => (
key: idx, <Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
color: LogLevelHierarchy(log.level),
children: (
<Space wrap align="start" style={{}}> <Space wrap align="start" style={{}}>
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag> <Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")}</span> <span>{moment(log.timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
<Divider type="vertical" /> <Divider type="vertical" />
<span>{log.message}</span> <span>{log.message}</span>
</Space> </Space>
), </Timeline.Item>
}))} ))}
/> </Timeline>
); );
} }

View File

@@ -1,26 +1,27 @@
import {DeleteFilled, DownOutlined} from "@ant-design/icons"; import { DeleteFilled, DownOutlined } from "@ant-design/icons";
import { import {
Button, Button,
Card, Card,
Divider, Divider,
Dropdown, Dropdown,
Form, Form,
Input, Input,
InputNumber, InputNumber,
Select, Menu,
Space, Select,
Statistic, Space,
Switch, Statistic,
Typography, Switch,
Typography,
} from "antd"; } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import dayjs from "../../utils/day"; import moment from "moment";
import React from "react"; import React 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 {determineDmsType} from "../../pages/dms/dms.container"; import { determineDmsType } from "../../pages/dms/dms.container";
import {selectBodyshop} from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import i18n from "../../translations/i18n"; import i18n from "../../translations/i18n";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component"; import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component"; import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
@@ -29,425 +30,440 @@ 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";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm); export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
export function DmsPostForm({bodyshop, socket, job, logsRef}) { export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
const [form] = Form.useForm(); const [form] = Form.useForm();
const {t} = useTranslation(); const { t } = useTranslation();
const handlePayerSelect = (value, index) => { const handlePayerSelect = (value, index) => {
form.setFieldsValue({ form.setFieldsValue({
payers: form.getFieldValue("payers").map((payer, mapIndex) => { payers: form.getFieldValue("payers").map((payer, mapIndex) => {
if (index !== mapIndex) return payer; if (index !== mapIndex) return payer;
const cdkPayer = const cdkPayer =
bodyshop.cdk_configuration.payers && bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find((i) => i.name === value); bodyshop.cdk_configuration.payers.find((i) => i.name === value);
if (!cdkPayer) return payer; if (!cdkPayer) return payer;
return { return {
...cdkPayer, ...cdkPayer,
dms_acctnumber: cdkPayer.dms_acctnumber, dms_acctnumber: cdkPayer.dms_acctnumber,
controlnumber: job && job[cdkPayer.control_type], controlnumber: job && job[cdkPayer.control_type],
}; };
}), }),
});
};
const handleFinish = (values) => {
socket.emit(`${determineDmsType(bodyshop)}-export-job`, {
jobid: job.id,
txEnvelope: values,
});
console.log(logsRef);
if (logsRef) {
console.log("executing", logsRef);
logsRef.curent &&
logsRef.current.scrollIntoView({
behavior: "smooth",
}); });
}; }
};
const handleFinish = (values) => { return (
socket.emit(`${determineDmsType(bodyshop)}-export-job`, { <Card title={t("jobs.labels.dms.postingform")}>
jobid: job.id, <Form
txEnvelope: values, form={form}
}); layout="vertical"
console.log(logsRef); onFinish={handleFinish}
if (logsRef) { initialValues={{
console.log("executing", logsRef); story: `${t("jobs.labels.dms.defaultstory", {
logsRef.curent && ro_number: job.ro_number,
logsRef.current.scrollIntoView({ ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
behavior: "smooth", job.ownr_co_nm || ""
}); }`.trim(),
} ins_co_nm: job.ins_co_nm || "N/A",
}; clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${
job.po_number || ""
}`,
}).trim()}.${
job.area_of_damage && job.area_of_damage.impact1
? " " +
t("jobs.labels.dms.damageto", {
area_of_damage:
(job.area_of_damage && job.area_of_damage.impact1) ||
"UNKNOWN",
})
: ""
}`.substr(0, 239),
inservicedate: moment("2019-01-01"),
}}
>
<LayoutFormRow grow>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="kmin"
label={t("jobs.fields.kmin")}
initialValue={job && job.kmin}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled />
</Form.Item>
<Form.Item
name="kmout"
label={t("jobs.fields.kmout")}
initialValue={job && job.kmout}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled />
</Form.Item>
</LayoutFormRow>
return ( {bodyshop.cdk_dealerid && (
<Card title={t("jobs.labels.dms.postingform")}> <div>
<Form <LayoutFormRow style={{ justifyContent: "center" }} grow>
form={form} <Form.Item
layout="vertical" name="dms_make"
onFinish={handleFinish} label={t("jobs.fields.dms.dms_make")}
initialValues={{ rules={[
story: `${t("jobs.labels.dms.defaultstory", { {
ro_number: job.ro_number, required: true,
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${ },
job.ownr_co_nm || "" ]}
}`.trim(), >
ins_co_nm: job.ins_co_nm || "N/A", <Input disabled />
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${ </Form.Item>
job.po_number || "" <Form.Item
}`, name="dms_model"
}).trim()}.${ label={t("jobs.fields.dms.dms_model")}
job.area_of_damage && job.area_of_damage.impact1 rules={[
? " " + {
t("jobs.labels.dms.damageto", { required: true,
area_of_damage: },
(job.area_of_damage && job.area_of_damage.impact1) || ]}
"UNKNOWN", >
}) <Input disabled />
: "" </Form.Item>
}`.slice(0, 239), <Form.Item
inservicedate: dayjs("2019-01-01"), name="inservicedate"
}} label={t("jobs.fields.dms.inservicedate")}
> >
<LayoutFormRow grow> <FormDatePicker />
<Form.Item </Form.Item>
name="journal" </LayoutFormRow>
label={t("jobs.fields.dms.journal")} <Space>
initialValue={ <DmsCdkMakes form={form} socket={socket} job={job} />
bodyshop.cdk_configuration && <DmsCdkMakesRefetch />
bodyshop.cdk_configuration.default_journal <Form.Item
} name="dms_unsold"
rules={[ label={t("jobs.fields.dms.dms_unsold")}
{ initialValue={false}
required: true, >
//message: t("general.validation.required"), <Switch />
}, </Form.Item>
]} <Form.Item
> name="dms_model_override"
<Input/> label={t("jobs.fields.dms.dms_model_override")}
</Form.Item> initialValue={false}
<Form.Item >
name="kmin" <Switch />
label={t("jobs.fields.kmin")} </Form.Item>
initialValue={job && job.kmin} </Space>
rules={[ </div>
{ )}
required: true, <Form.Item
//message: t("general.validation.required"), name="story"
}, label={t("jobs.fields.dms.story")}
]} rules={[
> {
<InputNumber disabled/> required: true,
</Form.Item> },
<Form.Item ]}
name="kmout" >
label={t("jobs.fields.kmout")} <Input.TextArea maxLength={240} />
initialValue={job && job.kmout} </Form.Item>
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled/>
</Form.Item>
</LayoutFormRow>
{bodyshop.cdk_dealerid && ( <Divider />
<div> <Space size="large" wrap align="center">
<LayoutFormRow style={{justifyContent: "center"}} grow> <Statistic
<Form.Item title={t("jobs.fields.ded_amt")}
name="dms_make" value={Dinero(
label={t("jobs.fields.dms.dms_make")} job.job_totals.totals.custPayable.deductible
rules={[ ).toFormat()}
{ />
required: true, <Statistic
}, title={t("jobs.labels.total_cust_payable")}
]} value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
> />
<Input disabled/> <Statistic
</Form.Item> title={t("jobs.labels.net_repairs")}
<Form.Item value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
name="dms_model" />
label={t("jobs.fields.dms.dms_model")} </Space>
rules={[ <Form.List name={["payers"]}>
{ {(fields, { add, remove }) => {
required: true, return (
}, <div>
]} {fields.map((field, index) => (
> <Form.Item key={field.key}>
<Input disabled/> <Space wrap>
</Form.Item> <Form.Item
<Form.Item label={t("jobs.fields.dms.payer.name")}
name="inservicedate" key={`${index}name`}
label={t("jobs.fields.dms.inservicedate")} name={[field.name, "name"]}
> rules={[
<FormDatePicker/> {
</Form.Item>
</LayoutFormRow>
<Space>
<DmsCdkMakes form={form} socket={socket} job={job}/>
<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>
</div>
)}
<Form.Item
name="story"
label={t("jobs.fields.dms.story")}
rules={[
{
required: true, required: true,
}, },
]} ]}
>
<Select
style={{ minWidth: "15rem" }}
onSelect={(value) => handlePayerSelect(value, index)}
>
{bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.map((payer) => (
<Select.Option key={payer.name}>
{payer.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0} />
</Form.Item>
<Form.Item
label={
<div>
{t("jobs.fields.dms.payer.controlnumber")}{" "}
<Dropdown
overlay={
<Menu>
{bodyshop.cdk_configuration.controllist &&
bodyshop.cdk_configuration.controllist.map(
(key, idx) => (
<Menu.Item
key={idx}
onClick={() => {
form.setFieldsValue({
payers: form
.getFieldValue("payers")
.map((row, mapIndex) => {
if (index !== mapIndex)
return row;
return {
...row,
controlnumber:
key.controlnumber,
};
}),
});
}}
>
{key.name}
</Menu.Item>
)
)}
</Menu>
}
>
<a href=" #" onClick={(e) => e.preventDefault()}>
<DownOutlined />
</a>
</Dropdown>
</div>
}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const payers = form.getFieldValue("payers");
const row = payers && payers[index];
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find(
(i) => i && row && i.name === row.name
);
if (
i18n.exists(`jobs.fields.${cdkPayer?.control_type}`)
)
return (
<div>
{cdkPayer &&
t(`jobs.fields.${cdkPayer?.control_type}`)}
</div>
);
else if (
i18n.exists(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)
) {
return (
<div>
{cdkPayer &&
t(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)}
</div>
);
} else {
return null;
}
}}
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={!(fields.length < 3)}
onClick={() => {
if (fields.length < 3) add();
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({ amount: Math.round((payer?.amount || 0) * 100) })
);
});
const totals =
socket.allocationsSummary &&
socket.allocationsSummary.reduce(
(acc, val) => {
return {
totalSale: acc.totalSale.add(Dinero(val.sale)),
totalCost: acc.totalCost.add(Dinero(val.cost)),
};
},
{
totalSale: Dinero(),
totalCost: Dinero(),
}
);
const discrep = totals
? totals.totalSale.subtract(totalAllocated)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.subtotal")}
value={(totals ? totals.totalSale : Dinero()).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.dms.notallocated")}
valueStyle={{
color: discrep.getAmount() === 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
<Button
disabled={
!socket.allocationsSummary || discrep.getAmount() !== 0
}
htmlType="submit"
> >
<Input.TextArea maxLength={240}/> {t("jobs.actions.dms.post")}
</Form.Item> </Button>
</Space>
<Divider/> );
<Space size="large" wrap align="center"> }}
<Statistic </Form.Item>
title={t("jobs.fields.ded_amt")} </Form>
value={Dinero( </Card>
job.job_totals.totals.custPayable.deductible );
).toFormat()}
/>
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
/>
<Statistic
title={t("jobs.labels.net_repairs")}
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
/>
</Space>
<Form.List name={["payers"]}>
{(fields, {add, remove}) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space wrap>
<Form.Item
label={t("jobs.fields.dms.payer.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Select
style={{minWidth: "15rem"}}
onSelect={(value) => handlePayerSelect(value, index)}
>
{bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.map((payer) => (
<Select.Option key={payer.name}>
{payer.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[
{
required: true,
},
]}
>
<Input disabled/>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0}/>
</Form.Item>
<Form.Item
label={
<div>
{t("jobs.fields.dms.payer.controlnumber")}{" "}
<Dropdown menu={{
items: bodyshop.cdk_configuration.controllist?.map((key, idx) => ({
key: idx,
label: key.name,
onClick: () => {
form.setFieldsValue({
payers: form.getFieldValue("payers").map((row, mapIndex) => {
if (index !== mapIndex) return row;
return {
...row,
controlnumber: key.controlnumber,
};
}),
});
},
}))
}}>
<a href=" #" onClick={(e) => e.preventDefault()}>
<DownOutlined/>
</a>
</Dropdown>
</div>
}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
]}
>
<Input/>
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const payers = form.getFieldValue("payers");
const row = payers && payers[index];
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find(
(i) => i && row && i.name === row.name
);
if (
i18n.exists(`jobs.fields.${cdkPayer?.control_type}`)
)
return (
<div>
{cdkPayer &&
t(`jobs.fields.${cdkPayer?.control_type}`)}
</div>
);
else if (
i18n.exists(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)
) {
return (
<div>
{cdkPayer &&
t(
`jobs.fields.dms.control_type.${cdkPayer?.control_type}`
)}
</div>
);
} else {
return null;
}
}}
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={!(fields.length < 3)}
onClick={() => {
if (fields.length < 3) add();
}}
style={{width: "100%"}}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({amount: Math.round((payer?.amount || 0) * 100)})
);
});
const totals =
socket.allocationsSummary &&
socket.allocationsSummary.reduce(
(acc, val) => {
return {
totalSale: acc.totalSale.add(Dinero(val.sale)),
totalCost: acc.totalCost.add(Dinero(val.cost)),
};
},
{
totalSale: Dinero(),
totalCost: Dinero(),
}
);
const discrep = totals
? totals.totalSale.subtract(totalAllocated)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.subtotal")}
value={(totals ? totals.totalSale : Dinero()).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.dms.notallocated")}
valueStyle={{
color: discrep.getAmount() === 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
<Button
disabled={
!socket.allocationsSummary || discrep.getAmount() !== 0
}
htmlType="submit"
>
{t("jobs.actions.dms.post")}
</Button>
</Space>
);
}}
</Form.Item>
</Form>
</Card>
);
} }

View File

@@ -60,10 +60,9 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
markerArea.current = new markerjs2.MarkerArea(imgRef.current); markerArea.current = new markerjs2.MarkerArea(imgRef.current);
// attach an event handler to assign annotated image back to our image element // attach an event handler to assign annotated image back to our image element
markerArea.current.addEventListener("close", (closeEvent) => {}); markerArea.current.addCloseEventListener((closeEvent) => {});
markerArea.current.addEventListener("render", (event) => { markerArea.current.addRenderEventListener((dataUrl) => {
const dataUrl = event.dataUrl;
imgRef.current.src = dataUrl; imgRef.current.src = dataUrl;
markerArea.current.close(); markerArea.current.close();
triggerUpload(dataUrl); triggerUpload(dataUrl);

View File

@@ -4,7 +4,7 @@ import queryString from "query-string";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries"; import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries"; import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries";
import { setBodyshop } from "../../redux/user/user.actions"; import { setBodyshop } from "../../redux/user/user.actions";

Some files were not shown because too many files have changed in this diff Show More