Merge remote-tracking branch 'origin/master-AIO' into feature/AIO/IO-2776-cdk-fortellis
This commit is contained in:
@@ -2,7 +2,7 @@ NGROK TEsting:
|
|||||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||||
|
|
||||||
Finding deadfiles - run from client directory
|
Finding deadfiles - run from client directory
|
||||||
npx deadfile ./src/index.js --exclude build templates
|
npx deadfile ./src/index.jsx --exclude build templates
|
||||||
|
|
||||||
#Crushing all hasura migrations by creating a new initialization from the server.
|
#Crushing all hasura migrations by creating a new initialization from the server.
|
||||||
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||||
|
|||||||
41
_reference/productionBoardNotes.md
Normal file
41
_reference/productionBoardNotes.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Production Board Notes:
|
||||||
|
|
||||||
|
## General Notes
|
||||||
|
|
||||||
|
- You can single click the lane footer to collapse/un-collapse the lane
|
||||||
|
- You can double click the lane header to collapse/un-collapse the lane
|
||||||
|
- If you need to scroll horizontally, you can hold shift and use the mouse scroll wheel, or press the mouse scroll wheel while scrolling
|
||||||
|
|
||||||
|
## Board Settings
|
||||||
|
|
||||||
|
#### Layout
|
||||||
|
|
||||||
|
- Board Orientation (Vertical or Horizontal)
|
||||||
|
- This determines the orientation of the card layout on the board.
|
||||||
|
- Horizontal is the default setting, and how the prior board was set up.
|
||||||
|
- Vertical is the new setting and allows lanes to be displayed vertically, with a grid of cards
|
||||||
|
- Card Size (Small, Medium, Large)
|
||||||
|
- This determines the size of the cards on the board.
|
||||||
|
- Small is the default setting, and how the prior board was set up.
|
||||||
|
- Medium and Large are new settings and allow for larger cards to be displayed on the board.
|
||||||
|
- Compact Cards (Tall or Wide)
|
||||||
|
- Formally called 'Compact'
|
||||||
|
- When on, data is displayed on the card vertically
|
||||||
|
- when turned off, some fields may share horizontal space, tightening the card layout
|
||||||
|
- Colored Cards (On or Off)
|
||||||
|
- When on, cards are colored based on the Status color
|
||||||
|
- Kiosk Mode (On or Off)
|
||||||
|
- This should be turned on if the shop is using it on a tablet (Ipad)
|
||||||
|
|
||||||
|
#### Information
|
||||||
|
|
||||||
|
These allow users to turn fields on or off, turning them all off will show the card in the most minimal form
|
||||||
|
|
||||||
|
|
||||||
|
### Statistics
|
||||||
|
|
||||||
|
- The statistics section allows users to see accumulations of both jobs on the board, and jobs in production.
|
||||||
|
- you can click a statistic to turn it on and off, and drag and drop the statistics to rearrange them
|
||||||
|
|
||||||
|
### Filters
|
||||||
|
- Allows you to set, and persist filters for estimators and insurance companies
|
||||||
@@ -4,7 +4,7 @@ Clone Repository for:
|
|||||||
{
|
{
|
||||||
"name": "node-webhook-scripts",
|
"name": "node-webhook-scripts",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.jsx",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.16.4"
|
"express": "^4.16.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ module.exports = {
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: "Bitbucket Webhook",
|
name: "Bitbucket Webhook",
|
||||||
script: "./webhook/index.js",
|
script: "./webhook/index.jsx",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production"
|
NODE_ENV: "production"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
||||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
||||||
VITE_APP_GA_CODE=231099835
|
VITE_APP_GA_CODE=231099835
|
||||||
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
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"}
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
||||||
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
// craco.config.js
|
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
|
||||||
const CracoLessPlugin = require("craco-less");
|
|
||||||
const { convertLegacyToken } = require("@ant-design/compatible/lib");
|
|
||||||
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 = {
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
devtool: "source-map"
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example plugins/index.js can be used to load plugins
|
// This example plugins/index.jsx can be used to load plugins
|
||||||
//
|
//
|
||||||
// You can change the location of this file or turn off loading
|
// You can change the location of this file or turn off loading
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example support/index.js is processed and
|
// This example support/index.jsx is processed and
|
||||||
// loaded automatically before your test files.
|
// loaded automatically before your test files.
|
||||||
//
|
//
|
||||||
// This is a great place to put global configuration and
|
// This is a great place to put global configuration and
|
||||||
|
|||||||
22372
client/package-lock.json
generated
22372
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,90 +2,92 @@
|
|||||||
"name": "bodyshop",
|
"name": "bodyshop",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.18.2"
|
"node": ">=18.18.2"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/compatible": "^5.1.2",
|
"@ant-design/pro-layout": "^7.19.11",
|
||||||
"@ant-design/pro-layout": "^7.17.16",
|
"@apollo/client": "^3.10.8",
|
||||||
"@apollo/client": "^3.8.10",
|
"@emotion/is-prop-valid": "^1.3.0",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.2.2",
|
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@reduxjs/toolkit": "^2.2.6",
|
||||||
"@sentry/cli": "^2.28.6",
|
"@sentry/cli": "^2.32.2",
|
||||||
"@sentry/react": "^7.104.0",
|
"@sentry/react": "^7.114.0",
|
||||||
"@splitsoftware/splitio-react": "^1.11.0",
|
"@splitsoftware/splitio-react": "^1.12.0",
|
||||||
"@tanem/react-nprogress": "^5.0.51",
|
"@tanem/react-nprogress": "^5.0.51",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"antd": "^5.15.3",
|
"antd": "^5.19.3",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"apollo-link-sentry": "^3.3.0",
|
||||||
"axios": "^1.6.7",
|
"autosize": "^6.0.1",
|
||||||
"dayjs": "^1.11.10",
|
"axios": "^1.6.8",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"css-box-model": "^1.2.1",
|
||||||
|
"dayjs": "^1.11.12",
|
||||||
"dayjs-business-days2": "^1.2.2",
|
"dayjs-business-days2": "^1.2.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^10.8.1",
|
"firebase": "^10.12.4",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.9.0",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.12.2",
|
||||||
"i18next-browser-languagedetector": "^7.0.2",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"libphonenumber-js": "^1.10.57",
|
"immutability-helper": "^3.1.1",
|
||||||
"logrocket": "^8.0.1",
|
"libphonenumber-js": "^1.11.4",
|
||||||
"markerjs2": "^2.32.0",
|
"logrocket": "^8.1.1",
|
||||||
"normalize-url": "^8.0.0",
|
"markerjs2": "^2.32.1",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"normalize-url": "^8.0.1",
|
||||||
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.0.0",
|
||||||
"react": "^18.2.0",
|
"raf-schd": "^4.0.3",
|
||||||
"react-big-calendar": "^1.11.0",
|
"react": "^18.3.1",
|
||||||
|
"react-big-calendar": "^1.13.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.1.0",
|
"react-cookie": "^7.1.4",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.0",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.1.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.2.1",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-joyride": "^2.7.4",
|
"react-joyride": "^2.8.2",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-number-format": "^5.3.3",
|
"react-number-format": "^5.4.0",
|
||||||
|
"react-popopo": "^2.1.9",
|
||||||
"react-product-fruits": "^2.2.6",
|
"react-product-fruits": "^2.2.6",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.2",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.22.2",
|
"react-router-dom": "^6.25.1",
|
||||||
"react-scripts": "^5.0.1",
|
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"recharts": "^2.12.2",
|
"react-virtuoso": "^4.7.12",
|
||||||
|
"recharts": "^2.12.7",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
|
"redux-actions": "^3.0.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.0",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.77.8",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.5",
|
||||||
"styled-components": "^6.1.8",
|
"styled-components": "^6.1.12",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.1",
|
"userpilot": "^1.3.2",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2"
|
||||||
"workbox-core": "^7.0.0",
|
|
||||||
"workbox-expiration": "^7.0.0",
|
|
||||||
"workbox-navigation-preload": "^7.0.0",
|
|
||||||
"workbox-precaching": "^7.0.0",
|
|
||||||
"workbox-routing": "^7.0.0",
|
|
||||||
"workbox-strategies": "^7.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
||||||
@@ -128,32 +130,30 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^0.15.4",
|
"@dotenvx/dotenvx": "^1.6.4",
|
||||||
"@emotion/babel-plugin": "^11.11.0",
|
"@emotion/babel-plugin": "^11.12.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.12.0",
|
||||||
"@sentry/webpack-plugin": "^2.14.2",
|
"@sentry/webpack-plugin": "^2.21.1",
|
||||||
"@swc/core": "^1.3.107",
|
"@testing-library/cypress": "^10.0.2",
|
||||||
"@swc/plugin-styled-components": "^1.5.108",
|
"browserslist": "^4.23.2",
|
||||||
"@testing-library/cypress": "^10.0.1",
|
|
||||||
"browserslist": "^4.22.3",
|
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^13.6.6",
|
"cypress": "^13.13.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"memfs": "^4.6.0",
|
"memfs": "^4.9.3",
|
||||||
"os-browserify": "^0.3.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.3",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.3.4",
|
||||||
"vite-plugin-babel": "^1.2.0",
|
"vite-plugin-babel": "^1.2.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-legacy": "^2.1.0",
|
"vite-plugin-legacy": "^2.1.0",
|
||||||
"vite-plugin-node-polyfills": "^0.19.0",
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"vite-plugin-pwa": "^0.19.0",
|
"vite-plugin-pwa": "^0.20.0",
|
||||||
"vite-plugin-style-import": "^2.0.0"
|
"vite-plugin-style-import": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method
|
|||||||
$ npm install --save-dev stubs
|
$ npm install --save-dev stubs
|
||||||
```
|
```
|
||||||
```js
|
```js
|
||||||
var mylib = require('./lib/index.js')
|
var mylib = require('./lib/index.jsx')
|
||||||
var stubs = require('stubs')
|
var stubs = require('stubs')
|
||||||
|
|
||||||
// make it a noop
|
// make it a noop
|
||||||
|
|||||||
@@ -16567,7 +16567,7 @@ even more slower.
|
|||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ node benchmarks/index.js
|
$ node benchmarks/index.jsx
|
||||||
Benchmarking: sign
|
Benchmarking: sign
|
||||||
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
||||||
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
||||||
|
|||||||
56
client/public/firebase-messaging-sw.js
Normal file
56
client/public/firebase-messaging-sw.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Scripts for firebase and firebase messaging
|
||||||
|
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
|
||||||
|
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
|
||||||
|
|
||||||
|
// Initialize the Firebase app in the service worker by passing the generated config
|
||||||
|
let firebaseConfig;
|
||||||
|
switch (this.location.hostname) {
|
||||||
|
case "localhost":
|
||||||
|
firebaseConfig = {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "test.imex.online":
|
||||||
|
firebaseConfig = {
|
||||||
|
apiKey: "AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c",
|
||||||
|
authDomain: "imex-test.firebaseapp.com",
|
||||||
|
projectId: "imex-test",
|
||||||
|
storageBucket: "imex-test.appspot.com",
|
||||||
|
messagingSenderId: "991923618608",
|
||||||
|
appId: "1:991923618608:web:633437569cdad78299bef5",
|
||||||
|
// measurementId: "${config.measurementId}",
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case "imex.online":
|
||||||
|
default:
|
||||||
|
firebaseConfig = {
|
||||||
|
apiKey: "AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU",
|
||||||
|
authDomain: "imex-prod.firebaseapp.com",
|
||||||
|
databaseURL: "https://imex-prod.firebaseio.com",
|
||||||
|
projectId: "imex-prod",
|
||||||
|
storageBucket: "imex-prod.appspot.com",
|
||||||
|
messagingSenderId: "253497221485",
|
||||||
|
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
||||||
|
measurementId: "G-NTWBKG2L0M",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
firebase.initializeApp(firebaseConfig);
|
||||||
|
|
||||||
|
// Retrieve firebase messaging
|
||||||
|
const messaging = firebase.messaging();
|
||||||
|
|
||||||
|
messaging.onBackgroundMessage(function (payload) {
|
||||||
|
// Customize notification here
|
||||||
|
const channel = new BroadcastChannel("imex-sw-messages");
|
||||||
|
channel.postMessage(payload);
|
||||||
|
|
||||||
|
//self.registration.showNotification(notificationTitle, notificationOptions);
|
||||||
|
});
|
||||||
@@ -7,9 +7,7 @@ import { connect } from "react-redux";
|
|||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } 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";
|
||||||
@@ -23,20 +21,21 @@ import "./App.styles.scss";
|
|||||||
import handleBeta from "../utils/betaHandler";
|
import handleBeta from "../utils/betaHandler";
|
||||||
import Eula from "../components/eula/eula.component";
|
import Eula from "../components/eula/eula.component";
|
||||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
import { ProductFruits } from "react-product-fruits";
|
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||||
|
|
||||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||||
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
|
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
online: selectOnline,
|
online: selectOnline,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentEula: selectCurrentEula
|
currentEula: selectCurrentEula
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession()),
|
checkUserSession: () => dispatch(checkUserSession()),
|
||||||
setOnline: (isOnline) => dispatch(setOnline(isOnline))
|
setOnline: (isOnline) => dispatch(setOnline(isOnline))
|
||||||
@@ -60,11 +59,11 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
|
|
||||||
// Associate event listeners, memoize to prevent multiple listeners being added
|
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const offlineListener = (e) => {
|
const offlineListener = () => {
|
||||||
setOnline(false);
|
setOnline(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onlineListener = (e) => {
|
const onlineListener = () => {
|
||||||
setOnline(true);
|
setOnline(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,24 +110,20 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
|
|
||||||
handleBeta();
|
handleBeta();
|
||||||
|
|
||||||
if (!online)
|
if (!online) {
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
status="warning"
|
status="warning"
|
||||||
title={t("general.labels.nointernet")}
|
title={t("general.labels.nointernet")}
|
||||||
subTitle={t("general.labels.nointernet_sub")}
|
subTitle={t("general.labels.nointernet_sub")}
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Button type="primary" onClick={() => window.location.reload()}>
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.refresh")}
|
{t("general.actions.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (currentEula && !currentUser.eulaIsAccepted) {
|
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||||
return <Eula />;
|
return <Eula />;
|
||||||
@@ -147,18 +142,13 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ProductFruits
|
<ProductFruitsWrapper
|
||||||
|
currentUser={currentUser}
|
||||||
workspaceCode={InstanceRenderMgr({
|
workspaceCode={InstanceRenderMgr({
|
||||||
imex: null,
|
imex: null,
|
||||||
rome: null,
|
rome: "9BkbEseqNqxw8jUH",
|
||||||
promanager: "aoJoEifvezYI0Z0P"
|
promanager: "aoJoEifvezYI0Z0P"
|
||||||
})}
|
})}
|
||||||
debug
|
|
||||||
language="en"
|
|
||||||
user={{
|
|
||||||
email: currentUser.email,
|
|
||||||
username: currentUser.email
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
|
|||||||
@@ -161,3 +161,15 @@
|
|||||||
.rowWithColor > td {
|
.rowWithColor > td {
|
||||||
background-color: var(--bgColor) !important;
|
background-color: var(--bgColor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.muted-button {
|
||||||
|
color: lightgray;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px; /* Adjust as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-button:hover {
|
||||||
|
color: darkgrey;
|
||||||
|
}
|
||||||
|
|||||||
32
client/src/App/ProductFruitsWrapper.jsx
Normal file
32
client/src/App/ProductFruitsWrapper.jsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ProductFruits } from "react-product-fruits";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||||
|
return (
|
||||||
|
workspaceCode &&
|
||||||
|
currentUser?.authorized === true &&
|
||||||
|
currentUser?.email && (
|
||||||
|
<ProductFruits
|
||||||
|
lifeCycle="unmount"
|
||||||
|
workspaceCode={workspaceCode}
|
||||||
|
debug
|
||||||
|
language="en"
|
||||||
|
user={{
|
||||||
|
email: currentUser.email,
|
||||||
|
username: currentUser.email
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ProductFruitsWrapper;
|
||||||
|
|
||||||
|
ProductFruitsWrapper.propTypes = {
|
||||||
|
currentUser: PropTypes.shape({
|
||||||
|
authorized: PropTypes.bool,
|
||||||
|
email: PropTypes.string
|
||||||
|
}),
|
||||||
|
workspaceCode: PropTypes.string
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
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";
|
||||||
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
|||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
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";
|
||||||
@@ -22,7 +23,6 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
|
|||||||
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
|
||||||
@@ -153,6 +153,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
|||||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||||
|
|
||||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||||
|
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -188,7 +189,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
||||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse} />
|
||||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
||||||
{bodyshop.uselocalmediaserver ? (
|
{bodyshop.uselocalmediaserver ? (
|
||||||
<JobsDocumentsLocalGallery
|
<JobsDocumentsLocalGallery
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export function BillFormComponent({
|
|||||||
job,
|
job,
|
||||||
loadOutstandingReturns,
|
loadOutstandingReturns,
|
||||||
loadInventory,
|
loadInventory,
|
||||||
preferredMake
|
preferredMake,
|
||||||
|
disableInHouse
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -177,7 +178,7 @@ export function BillFormComponent({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<VendorSearchSelect
|
<VendorSearchSelect
|
||||||
disabled={disabled}
|
disabled={disabled || disableInHouse}
|
||||||
options={vendorAutoCompleteOptions}
|
options={vendorAutoCompleteOptions}
|
||||||
preferredMake={preferredMake}
|
preferredMake={preferredMake}
|
||||||
onSelect={handleVendorSelect}
|
onSelect={handleVendorSelect}
|
||||||
@@ -243,7 +244,7 @@ export function BillFormComponent({
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input disabled={disabled || disableInvNumber} />
|
<Input disabled={disabled || disableInvNumber || disableInHouse} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bills.fields.date")}
|
label={t("bills.fields.date")}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
|
||||||
const {
|
const {
|
||||||
treatments: { Simple_Inventory }
|
treatments: { Simple_Inventory }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
@@ -47,6 +47,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
|
|||||||
job={lineData ? lineData.jobs_by_pk : null}
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
disableInvNumber={disableInvNumber}
|
disableInvNumber={disableInvNumber}
|
||||||
|
disableInHouse={disableInHouse}
|
||||||
loadOutstandingReturns={loadOutstandingReturns}
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
loadInventory={loadInventory}
|
loadInventory={loadInventory}
|
||||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import React, { forwardRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
//To be used as a form element only.
|
|
||||||
const { Option } = Select;
|
|
||||||
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -25,31 +23,27 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
notFoundContent={"Removed."}
|
notFoundContent={"Removed."}
|
||||||
{...restProps}
|
options={[
|
||||||
>
|
{ value: "noline", label: t("billlines.labels.other"), name: t("billlines.labels.other") },
|
||||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
...options.map((item) => ({
|
||||||
{t("billlines.labels.other")}
|
disabled: allowRemoved ? false : item.removed,
|
||||||
</Select.Option>
|
key: item.id,
|
||||||
{options
|
value: item.id,
|
||||||
? options.map((item) => (
|
cost: item.act_price ? item.act_price : 0,
|
||||||
<Option
|
part_type: item.part_type,
|
||||||
disabled={allowRemoved ? false : item.removed}
|
line_desc: item.line_desc,
|
||||||
key={item.id}
|
part_qty: item.part_qty,
|
||||||
value={item.id}
|
oem_partno: item.oem_partno,
|
||||||
cost={item.act_price ? item.act_price : 0}
|
alt_partno: item.alt_partno,
|
||||||
part_type={item.part_type}
|
act_price: item.act_price,
|
||||||
line_desc={item.line_desc}
|
style: {
|
||||||
part_qty={item.part_qty}
|
|
||||||
oem_partno={item.oem_partno}
|
|
||||||
alt_partno={item.alt_partno}
|
|
||||||
act_price={item.act_price}
|
|
||||||
style={{
|
|
||||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||||
}}
|
},
|
||||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||||
>
|
label: (
|
||||||
|
<>
|
||||||
<span>
|
<span>
|
||||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
@@ -60,14 +54,15 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
|
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
<span style={{ float: "right", paddingleft: "1rem" }}>
|
||||||
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||||
</span>
|
</span>
|
||||||
</Option>
|
</>
|
||||||
))
|
)
|
||||||
: null}
|
}))
|
||||||
</Select>
|
]}
|
||||||
|
{...restProps}
|
||||||
|
></Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default forwardRef(BillLineSearchSelect);
|
export default forwardRef(BillLineSearchSelect);
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||||
@@ -28,12 +26,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||||
const { context } = cardPaymentModal;
|
const { context, actions } = cardPaymentModal;
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
skip: true
|
skip: true
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
|
||||||
//Initialize the intellipay window.
|
//Initialize the intellipay window.
|
||||||
const SetIntellipayCallbackFunctions = () => {
|
const SetIntellipayCallbackFunctions = () => {
|
||||||
console.log("*** Set IntelliPay callback functions.");
|
console.log("*** Set IntelliPay callback functions.");
|
||||||
@@ -51,16 +48,20 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnApproval(async function (response) {
|
window.intellipay.runOnApproval(async function (response) {
|
||||||
console.warn("*** Running On Approval Script ***");
|
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||||
form.setFieldValue("paymentResponse", response);
|
//Add a slight delay to allow the refetch to properly get the data.
|
||||||
form.submit();
|
setTimeout(() => {
|
||||||
|
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||||
|
actions.refetch();
|
||||||
|
setLoading(false);
|
||||||
|
toggleModalVisible();
|
||||||
|
}, 750);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnNonApproval(async function (response) {
|
window.intellipay.runOnNonApproval(async function (response) {
|
||||||
// Mutate unsuccessful payment
|
// Mutate unsuccessful payment
|
||||||
|
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
|
|
||||||
await insertPaymentResponse({
|
await insertPaymentResponse({
|
||||||
variables: {
|
variables: {
|
||||||
paymentResponse: payments.map((payment) => ({
|
paymentResponse: payments.map((payment) => ({
|
||||||
@@ -85,50 +86,9 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
|
||||||
try {
|
|
||||||
await insertPayment({
|
|
||||||
variables: {
|
|
||||||
paymentInput: values.payments.map((payment) => ({
|
|
||||||
amount: payment.amount,
|
|
||||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
|
||||||
payer: t("payments.labels.customer"),
|
|
||||||
type: values.paymentResponse.cardbrand,
|
|
||||||
jobid: payment.jobid,
|
|
||||||
date: dayjs(Date.now()),
|
|
||||||
payment_responses: {
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
amount: payment.amount,
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
|
|
||||||
jobid: payment.jobid,
|
|
||||||
declinereason: values.paymentResponse.declinereason,
|
|
||||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
|
||||||
successful: true,
|
|
||||||
response: values.paymentResponse
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
refetchQueries: ["GET_JOB_BY_PK"]
|
|
||||||
});
|
|
||||||
toggleModalVisible();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("payments.errors.inserting", { error: error.message })
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleIntelliPayCharge = async () => {
|
const handleIntelliPayCharge = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
//Validate
|
//Validate
|
||||||
try {
|
try {
|
||||||
await form.validateFields();
|
await form.validateFields();
|
||||||
@@ -140,7 +100,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||||
bodyshop,
|
bodyshop,
|
||||||
refresh: !!window.intellipay
|
refresh: !!window.intellipay,
|
||||||
|
paymentSplitMeta: form.getFieldsValue(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.intellipay) {
|
if (window.intellipay) {
|
||||||
@@ -169,7 +130,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
<Card title="Card Payment">
|
<Card title="Card Payment">
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
@@ -246,18 +206,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
console.log("Updating the owner info section.");
|
|
||||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
if (
|
||||||
console.log("**Calling refetch.");
|
payments?.length > 0 &&
|
||||||
|
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||||
|
) {
|
||||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||||
}
|
}
|
||||||
console.log(
|
|
||||||
"Acc info",
|
|
||||||
data,
|
|
||||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -300,6 +256,13 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
type="hidden"
|
type="hidden"
|
||||||
value={totalAmountToCharge?.toFixed(2)}
|
value={totalAmountToCharge?.toFixed(2)}
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
className="ipayfield"
|
||||||
|
data-ipayname="comment"
|
||||||
|
type="hidden"
|
||||||
|
value={btoa(JSON.stringify(payments))}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
// data-ipayname="submit"
|
// data-ipayname="submit"
|
||||||
@@ -314,11 +277,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{/* Lightbox payment response when it is completed */}
|
|
||||||
<Form.Item name="paymentResponse" hidden>
|
|
||||||
<Input type="hidden" />
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
</Form>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
|||||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||||
|
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,7 +61,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
{
|
{
|
||||||
text: t("courtesycars.status.leasereturn"),
|
text: t("courtesycars.status.leasereturn"),
|
||||||
value: "courtesycars.status.leasereturn"
|
value: "courtesycars.status.leasereturn"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
text: t("courtesycars.status.unavailable"),
|
||||||
|
value: "courtesycars.status.unavailable",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onFilter: (value, record) => record.status === value,
|
onFilter: (value, record) => record.status === value,
|
||||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import axios from "axios";
|
|||||||
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
|
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
|
||||||
|
|
||||||
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
|
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
|
||||||
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [lifecycleData, setLifecycleData] = useState(null);
|
const [lifecycleData, setLifecycleData] = useState(null);
|
||||||
@@ -143,7 +142,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{lifecycleData.summations.map((key) => (
|
{lifecycleData.summations.map((key) => (
|
||||||
<Tag color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||||
<div
|
<div
|
||||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||||
@@ -165,6 +164,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
|||||||
size="small"
|
size="small"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
rowKey={(record) => record.status}
|
||||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
|
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -89,8 +89,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
|||||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
console.log("Render record out today");
|
|
||||||
console.dir(record);
|
|
||||||
return record.ownerid ? (
|
return record.ownerid ? (
|
||||||
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
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";
|
||||||
@@ -22,6 +21,7 @@ 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 dayjs from "../../utils/day";
|
||||||
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";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
@@ -89,7 +89,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
|||||||
job.area_of_damage && job.area_of_damage.impact1
|
job.area_of_damage && job.area_of_damage.impact1
|
||||||
? " " +
|
? " " +
|
||||||
t("jobs.labels.dms.damageto", {
|
t("jobs.labels.dms.damageto", {
|
||||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN"
|
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
|
||||||
})
|
})
|
||||||
: ""
|
: ""
|
||||||
}`.slice(0, 239),
|
}`.slice(0, 239),
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import axios from "axios";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||||
|
|
||||||
export default function GlobalSearchOs() {
|
export default function GlobalSearchOs() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState(false);
|
const [data, setData] = useState(false);
|
||||||
|
|
||||||
@@ -178,15 +177,7 @@ export default function GlobalSearchOs() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
|
||||||
options={data}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
defaultActiveFirstOption
|
|
||||||
onSelect={(val, opt) => {
|
|
||||||
history(opt.label.props.to);
|
|
||||||
}}
|
|
||||||
onClear={() => setData([])}
|
|
||||||
>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t("general.labels.globalsearch")}
|
placeholder={t("general.labels.globalsearch")}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -12,7 +12,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
|||||||
|
|
||||||
export default function GlobalSearch() {
|
export default function GlobalSearch() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const history = useNavigate();
|
|
||||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||||
|
|
||||||
const executeSearch = (v) => {
|
const executeSearch = (v) => {
|
||||||
@@ -157,14 +156,7 @@ export default function GlobalSearch() {
|
|||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
|
||||||
options={options}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
defaultActiveFirstOption
|
|
||||||
onSelect={(val, opt) => {
|
|
||||||
history(opt.label.props.to);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
size="large"
|
size="large"
|
||||||
placeholder={t("general.labels.globalsearch")}
|
placeholder={t("general.labels.globalsearch")}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { BsKanban } from "react-icons/bs";
|
import { BsKanban } from "react-icons/bs";
|
||||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||||
|
import { FiLogOut } from "react-icons/fi";
|
||||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||||
import { IoBusinessOutline } from "react-icons/io5";
|
import { IoBusinessOutline } from "react-icons/io5";
|
||||||
import { RiSurveyLine } from "react-icons/ri";
|
import { RiSurveyLine } from "react-icons/ri";
|
||||||
@@ -42,7 +43,6 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
|
|||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { FiLogOut } from "react-icons/fi";
|
|
||||||
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
@@ -141,11 +141,13 @@ function Header({
|
|||||||
accountingChildren.push(
|
accountingChildren.push(
|
||||||
{
|
{
|
||||||
key: "bills",
|
key: "bills",
|
||||||
|
id: "header-accounting-bills",
|
||||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||||
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "enterbills",
|
key: "enterbills",
|
||||||
|
id: "header-accounting-enterbills",
|
||||||
icon: <Icon component={GiPayMoney} />,
|
icon: <Icon component={GiPayMoney} />,
|
||||||
label: t("menus.header.enterbills"),
|
label: t("menus.header.enterbills"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -165,6 +167,7 @@ function Header({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "inventory",
|
key: "inventory",
|
||||||
|
id: "header-accounting-inventory",
|
||||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||||
}
|
}
|
||||||
@@ -183,11 +186,13 @@ function Header({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "allpayments",
|
key: "allpayments",
|
||||||
|
id: "header-accounting-allpayments",
|
||||||
icon: <BankFilled />,
|
icon: <BankFilled />,
|
||||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "enterpayments",
|
key: "enterpayments",
|
||||||
|
id: "header-accounting-enterpayments",
|
||||||
icon: <Icon component={FaCreditCard} />,
|
icon: <Icon component={FaCreditCard} />,
|
||||||
label: t("menus.header.enterpayment"),
|
label: t("menus.header.enterpayment"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -203,6 +208,7 @@ function Header({
|
|||||||
if (ImEXPay.treatment === "on") {
|
if (ImEXPay.treatment === "on") {
|
||||||
accountingChildren.push({
|
accountingChildren.push({
|
||||||
key: "entercardpayments",
|
key: "entercardpayments",
|
||||||
|
id: "header-accounting-entercardpayments",
|
||||||
icon: <Icon component={FaCreditCard} />,
|
icon: <Icon component={FaCreditCard} />,
|
||||||
label: t("menus.header.entercardpayment"),
|
label: t("menus.header.entercardpayment"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -227,6 +233,7 @@ function Header({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "timetickets",
|
key: "timetickets",
|
||||||
|
id: "header-accounting-timetickets",
|
||||||
icon: <FieldTimeOutlined />,
|
icon: <FieldTimeOutlined />,
|
||||||
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
||||||
}
|
}
|
||||||
@@ -235,6 +242,7 @@ function Header({
|
|||||||
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
||||||
accountingChildren.push({
|
accountingChildren.push({
|
||||||
key: "ttapprovals",
|
key: "ttapprovals",
|
||||||
|
id: "header-accounting-ttapprovals",
|
||||||
icon: <FieldTimeOutlined />,
|
icon: <FieldTimeOutlined />,
|
||||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||||
});
|
});
|
||||||
@@ -244,6 +252,7 @@ function Header({
|
|||||||
key: "entertimetickets",
|
key: "entertimetickets",
|
||||||
icon: <Icon component={GiPlayerTime} />,
|
icon: <Icon component={GiPlayerTime} />,
|
||||||
label: t("menus.header.entertimeticket"),
|
label: t("menus.header.entertimeticket"),
|
||||||
|
id: "header-accounting-entertimetickets",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTimeTicketContext({
|
setTimeTicketContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
@@ -264,6 +273,7 @@ function Header({
|
|||||||
const accountingExportChildren = [
|
const accountingExportChildren = [
|
||||||
{
|
{
|
||||||
key: "receivables",
|
key: "receivables",
|
||||||
|
id: "header-accounting-receivables",
|
||||||
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
|
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@@ -271,6 +281,7 @@ function Header({
|
|||||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
|
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
|
||||||
accountingExportChildren.push({
|
accountingExportChildren.push({
|
||||||
key: "payables",
|
key: "payables",
|
||||||
|
id: "header-accounting-payables",
|
||||||
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link>
|
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -278,6 +289,7 @@ function Header({
|
|||||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
|
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
|
||||||
accountingExportChildren.push({
|
accountingExportChildren.push({
|
||||||
key: "payments",
|
key: "payments",
|
||||||
|
id: "header-accounting-payments",
|
||||||
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link>
|
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -288,6 +300,7 @@ function Header({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "exportlogs",
|
key: "exportlogs",
|
||||||
|
id: "header-accounting-exportlogs",
|
||||||
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link>
|
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -301,6 +314,7 @@ function Header({
|
|||||||
) {
|
) {
|
||||||
accountingChildren.push({
|
accountingChildren.push({
|
||||||
key: "accountingexport",
|
key: "accountingexport",
|
||||||
|
id: "header-accounting-export",
|
||||||
icon: <ExportOutlined />,
|
icon: <ExportOutlined />,
|
||||||
label: t("menus.header.export"),
|
label: t("menus.header.export"),
|
||||||
children: accountingExportChildren
|
children: accountingExportChildren
|
||||||
@@ -604,14 +618,22 @@ function Header({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// {
|
...(InstanceRenderManager({
|
||||||
// key: 'rescue',
|
imex: true,
|
||||||
// icon: <Icon component={CarFilled}/>,
|
rome: false,
|
||||||
// label: t("menus.header.rescueme"),
|
promanager: false
|
||||||
// onClick: () => {
|
})
|
||||||
// window.open("https://imexrescue.com/", "_blank");
|
? [
|
||||||
// }
|
{
|
||||||
// },
|
key: "rescue",
|
||||||
|
icon: <Icon component={CarFilled} />,
|
||||||
|
label: t("menus.header.rescueme"),
|
||||||
|
onClick: () => {
|
||||||
|
window.open("https://imexrescue.com/", "_blank");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
|
||||||
...(InstanceRenderManager({
|
...(InstanceRenderManager({
|
||||||
imex: true,
|
imex: true,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Form, Input, notification, Switch } from "antd";
|
import { Button, Card, Form, Input, notification, Switch } from "antd";
|
||||||
import dayjs from "../../../../utils/day";
|
|
||||||
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";
|
||||||
@@ -14,6 +13,7 @@ import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
|||||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||||
|
import dayjs from "../../../../utils/day";
|
||||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||||
|
|
||||||
@@ -275,7 +275,19 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
|||||||
>
|
>
|
||||||
<DateTimePicker disabled={readOnly} />
|
<DateTimePicker disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="actual_delivery" label={t("jobs.fields.actual_delivery")} disabled={readOnly}>
|
<Form.Item
|
||||||
|
name="actual_delivery"
|
||||||
|
label={t("jobs.fields.actual_delivery")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: bodyshop.deliverchecklist.actual_delivery
|
||||||
|
? bodyshop.deliverchecklist.actual_delivery
|
||||||
|
: false
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
<DateTimePicker disabled={readOnly} />
|
<DateTimePicker disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -19,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
|
||||||
|
|
||||||
export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) {
|
export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||||
const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, {
|
const { loading, error, data } = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
|
||||||
variables: { jobid: job.id },
|
variables: { jobid: job.id },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { LockOutlined } from "@ant-design/icons";
|
import { LockOutlined } from "@ant-design/icons";
|
||||||
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
|
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -12,9 +12,8 @@ import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
|
|||||||
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
|
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
|
||||||
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
|
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
|
||||||
import "./job-close-ro-guard.styles.scss";
|
import "./job-close-ro-guard.styles.scss";
|
||||||
import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
|
|
||||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
|
||||||
import Car from "../job-damage-visual/job-damage-visual.component";
|
import Car from "../job-damage-visual/job-damage-visual.component";
|
||||||
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
|
|
||||||
export default function JobDetailCardsDamageComponent({ loading, data }) {
|
export default function JobDetailCardsDamageComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { area_of_damage } = data;
|
const { area_of_damage } = data;
|
||||||
return (
|
return (
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
||||||
{area_of_damage ? <Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} /> : t("jobs.errors.nodamage")}
|
{area_of_damage ? (
|
||||||
|
<Car
|
||||||
|
dmg1={area_of_damage.impact1 && area_of_damage.impact1.padStart(2, "0")}
|
||||||
|
dmg2={area_of_damage.impact2 && area_of_damage.impact2.padStart(2, "0")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
t("jobs.errors.nodamage")
|
||||||
|
)}
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,16 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { joblines_status } = data;
|
const { joblines_status } = data;
|
||||||
|
|
||||||
|
const filteredJobLines = data.joblines.filter(
|
||||||
|
(j) =>
|
||||||
|
j.part_type !== null &&
|
||||||
|
j.part_type !== "PAE" &&
|
||||||
|
j.part_type !== "PAS" &&
|
||||||
|
j.part_type !== "PASL" &&
|
||||||
|
j.part_qty !== 0 &&
|
||||||
|
j.act_price !== 0
|
||||||
|
);
|
||||||
|
//TODO: Correct jobline_statuses view by including the part_qty !== 0 and act_price !== 0
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.line_desc"),
|
title: t("joblines.fields.line_desc"),
|
||||||
@@ -95,7 +105,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
|
|||||||
<div>
|
<div>
|
||||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
||||||
<PartsStatusPie joblines_status={joblines_status} />
|
<PartsStatusPie joblines_status={joblines_status} />
|
||||||
<Table key="id" columns={columns} dataSource={data ? data.joblines : []} />
|
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
|
||||||
</CardTemplate>
|
</CardTemplate>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,26 +2,30 @@ import { useQuery } from "@apollo/client";
|
|||||||
import { Col, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
import { Col, Row, Skeleton, Space, Timeline, Typography } 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 { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
|
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
|
||||||
|
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||||
|
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { connect } from "react-redux";
|
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
|
||||||
import { createStructuredSelector } from "reselect";
|
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
|
||||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
technician: selectTechnician
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
|
||||||
|
|
||||||
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
|
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
@@ -46,9 +50,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
children: (
|
children: (
|
||||||
<Row wrap>
|
<Row wrap>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
|
{!technician ? (
|
||||||
|
<>
|
||||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||||
{line.parts_order.order_number}
|
{line.parts_order.order_number}
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`${line.parts_order.order_number}`
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||||
@@ -83,30 +93,34 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
key: line.id,
|
key: line.id,
|
||||||
children: (
|
children: (
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={8}>
|
<Col span={8}>{line.parts_dispatch.number}</Col>
|
||||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
|
|
||||||
</Col>
|
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
{line.accepted_at ? (
|
||||||
<Space>
|
<Space>
|
||||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||||
</Space>
|
</Space>
|
||||||
|
) : null}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
: {
|
: [
|
||||||
|
{
|
||||||
key: "dispatch-lines",
|
key: "dispatch-lines",
|
||||||
children: t("parts_orders.labels.notyetordered")
|
children: t("parts_dispatch.labels.notyetdispatched")
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
<FeatureWrapper featureName="bills" noauth={() => null}>
|
||||||
<Col md={24} lg={8}>
|
<Col md={24} lg={8}>
|
||||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||||
|
<BillDetailEditcontainer />
|
||||||
<Timeline
|
<Timeline
|
||||||
items={
|
items={
|
||||||
data.billlines.length > 0
|
data.billlines.length > 0
|
||||||
@@ -115,9 +129,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
children: (
|
children: (
|
||||||
<Row wrap>
|
<Row wrap>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
|
{!technician ? (
|
||||||
|
<>
|
||||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||||
{line.bill.invoice_number}
|
{line.bill.invoice_number}
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`${line.bill.invoice_number}`
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<span>
|
<span>
|
||||||
@@ -147,6 +167,7 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
</FeatureWrapper>
|
||||||
<Col md={24} lg={24}>
|
<Col md={24} lg={24}>
|
||||||
<TaskListContainer
|
<TaskListContainer
|
||||||
parentJobId={jobid}
|
parentJobId={jobid}
|
||||||
|
|||||||
@@ -3,13 +3,21 @@ import { Button, Form, notification, Popover, Tooltip } from "antd";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { UPDATE_LINE_PPC } from "../../graphql/jobs-lines.queries";
|
import { UPDATE_LINE_PPC } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||||
|
|
||||||
export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
technician: selectTechnician
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
|
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
||||||
|
|
||||||
@@ -52,7 +60,7 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const popcontent = InstanceRenderManager({
|
const popcontent = !technician && InstanceRenderManager({
|
||||||
imex: null,
|
imex: null,
|
||||||
rome: (
|
rome: (
|
||||||
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
|
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
|
||||||
@@ -95,3 +103,4 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
|||||||
</JobLineConvertToLabor>
|
</JobLineConvertToLabor>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesPartPriceChange);
|
||||||
|
|||||||
@@ -31,18 +31,20 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
|||||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { FaTasks } from "react-icons/fa";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||||
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
|
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
|
||||||
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
||||||
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
|
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
|
||||||
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
|
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
|
||||||
|
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
|
||||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||||
import JobLinesExpander from "./job-lines-expander.component";
|
import JobLinesExpander from "./job-lines-expander.component";
|
||||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||||
import { FaTasks } from "react-icons/fa";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -53,6 +55,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
|
setPartsReceiveContext: (context) => dispatch(setModalContext({ context: context, modal: "partsReceive" })),
|
||||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||||
});
|
});
|
||||||
@@ -62,6 +65,7 @@ export function JobLinesComponent({
|
|||||||
jobRO,
|
jobRO,
|
||||||
technician,
|
technician,
|
||||||
setPartsOrderContext,
|
setPartsOrderContext,
|
||||||
|
setPartsReceiveContext,
|
||||||
loading,
|
loading,
|
||||||
refetch,
|
refetch,
|
||||||
jobLines,
|
jobLines,
|
||||||
@@ -70,7 +74,11 @@ export function JobLinesComponent({
|
|||||||
setJobLineEditContext,
|
setJobLineEditContext,
|
||||||
form,
|
form,
|
||||||
setBillEnterContext,
|
setBillEnterContext,
|
||||||
setTaskUpsertContext
|
setTaskUpsertContext,
|
||||||
|
billsQuery,
|
||||||
|
handleBillOnRowClick,
|
||||||
|
handlePartsOrderOnRowClick,
|
||||||
|
handlePartsDispatchOnRowClick
|
||||||
}) {
|
}) {
|
||||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||||
const {
|
const {
|
||||||
@@ -197,7 +205,6 @@ export function JobLinesComponent({
|
|||||||
onFilter: (value, record) => value.includes(record.part_type),
|
onFilter: (value, record) => value.includes(record.part_type),
|
||||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.act_price"),
|
title: t("joblines.fields.act_price"),
|
||||||
dataIndex: "act_price",
|
dataIndex: "act_price",
|
||||||
@@ -212,7 +219,6 @@ export function JobLinesComponent({
|
|||||||
dataIndex: "part_qty",
|
dataIndex: "part_qty",
|
||||||
key: "part_qty"
|
key: "part_qty"
|
||||||
},
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// title: t('joblines.fields.tax_part'),
|
// title: t('joblines.fields.tax_part'),
|
||||||
// dataIndex: 'tax_part',
|
// dataIndex: 'tax_part',
|
||||||
@@ -321,7 +327,7 @@ export function JobLinesComponent({
|
|||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
{(record.manual_line || jobIsPrivate) && (
|
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
@@ -336,7 +342,6 @@ export function JobLinesComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.create")}
|
title={t("tasks.buttons.create")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -350,7 +355,7 @@ export function JobLinesComponent({
|
|||||||
>
|
>
|
||||||
<FaTasks />
|
<FaTasks />
|
||||||
</Button>
|
</Button>
|
||||||
{(record.manual_line || jobIsPrivate) && (
|
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
@@ -436,6 +441,15 @@ export function JobLinesComponent({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PartsOrderModalContainer />
|
<PartsOrderModalContainer />
|
||||||
|
{!technician && (
|
||||||
|
<PartsOrderDrawer
|
||||||
|
job={job}
|
||||||
|
billsQuery={billsQuery}
|
||||||
|
handleOnRowClick={handlePartsOrderOnRowClick}
|
||||||
|
setPartsReceiveContext={setPartsReceiveContext}
|
||||||
|
setTaskUpsertContext={setTaskUpsertContext}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t("jobs.labels.estimatelines")}
|
title={t("jobs.labels.estimatelines")}
|
||||||
extra={
|
extra={
|
||||||
@@ -552,7 +566,7 @@ export function JobLinesComponent({
|
|||||||
>
|
>
|
||||||
{t("joblines.actions.new")}
|
{t("joblines.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
{bodyshop.region_config.toLowerCase().startsWith("us") && <JobSendPartPriceChangeComponent job={job} />}
|
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import JobLinesComponent from "./job-lines.component";
|
import JobLinesComponent from "./job-lines.component";
|
||||||
|
|
||||||
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
function JobLinesContainer({
|
||||||
|
job,
|
||||||
|
joblines,
|
||||||
|
billsQuery,
|
||||||
|
handleBillOnRowClick,
|
||||||
|
handlePartsOrderOnRowClick,
|
||||||
|
handlePartsDispatchOnRowClick,
|
||||||
|
refetch,
|
||||||
|
form,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const jobLines = useMemo(() => {
|
const jobLines = useMemo(() => {
|
||||||
@@ -22,7 +32,19 @@ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
|||||||
}, [joblines, searchText]);
|
}, [joblines, searchText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JobLinesComponent refetch={refetch} jobLines={jobLines} setSearchText={setSearchText} job={job} form={form} />
|
<div>
|
||||||
|
<JobLinesComponent
|
||||||
|
refetch={refetch}
|
||||||
|
jobLines={jobLines}
|
||||||
|
billsQuery={billsQuery}
|
||||||
|
handleBillOnRowClick={handleBillOnRowClick}
|
||||||
|
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
||||||
|
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
|
||||||
|
setSearchText={setSearchText}
|
||||||
|
job={job}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
technician: selectTechnician
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLineConvertToLabor);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobLineConvertToLabor);
|
||||||
|
|
||||||
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, ...otherBtnProps }) {
|
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, technician, ...otherBtnProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -165,7 +166,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
{jobline.act_price !== 0 && (
|
{jobline.act_price !== 0 && !technician && (
|
||||||
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
|
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
|
||||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function JobSendPartPriceChangeComponent({ job }) {
|
export default function JobSendPartPriceChangeComponent({ job, disabled }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
@@ -24,7 +24,7 @@ export default function JobSendPartPriceChangeComponent({ job }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleClick} loading={loading}>
|
<Button onClick={handleClick} loading={loading} disabled={disabled}>
|
||||||
{t("jobs.actions.sendpartspricechange")}
|
{t("jobs.actions.sendpartspricechange")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import Dinero from "dinero.js";
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
|
||||||
export default function JobTotalsTableLabor({ job }) {
|
export default function JobTotalsTableLabor({ job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -56,6 +56,8 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||||
render: (text, record) => record.hours.toFixed(1)
|
render: (text, record) => record.hours.toFixed(1)
|
||||||
},
|
},
|
||||||
|
...InstanceRenderManager({
|
||||||
|
imex: [
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.total"),
|
title: t("joblines.fields.total"),
|
||||||
dataIndex: "total",
|
dataIndex: "total",
|
||||||
@@ -63,9 +65,40 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
align: "right",
|
align: "right",
|
||||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
|
||||||
render: (text, record) => Dinero(record.total).toFormat()
|
render: (text, record) => Dinero(record.total).toFormat()
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
rome: [
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.amount"),
|
||||||
|
dataIndex: "base",
|
||||||
|
key: "base",
|
||||||
|
align: "right",
|
||||||
|
sorter: (a, b) => a.base.amount - b.base.amount,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "base" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => Dinero(record.base).toFormat()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.adjustment"),
|
||||||
|
dataIndex: "adjustment",
|
||||||
|
key: "adjustment",
|
||||||
|
align: "right",
|
||||||
|
sorter: (a, b) => a.adjustment.amount - b.adjustment.amount,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "adjustment" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => Dinero(record.adjustment).toFormat()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("joblines.fields.total"),
|
||||||
|
dataIndex: "total",
|
||||||
|
key: "total",
|
||||||
|
align: "right",
|
||||||
|
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => Dinero(record.total).toFormat()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
promanager: "USE_ROME"
|
||||||
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
@@ -91,6 +124,16 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell>
|
||||||
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
|
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
|
{InstanceRenderManager({
|
||||||
|
imex: null,
|
||||||
|
rome: (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
promanager: "USE_ROME"
|
||||||
|
})}
|
||||||
<Table.Summary.Cell align="right">
|
<Table.Summary.Cell align="right">
|
||||||
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
|
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
@@ -122,7 +165,29 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
|
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
|
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mapa.total).toFormat()}</Table.Summary.Cell>
|
{InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
rome: (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mapa.base).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
promanager: "USE_ROME"
|
||||||
|
})}
|
||||||
</Table.Summary.Row>
|
</Table.Summary.Row>
|
||||||
<Table.Summary.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell>
|
||||||
@@ -151,7 +216,29 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
|
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
|
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mash.total).toFormat()}</Table.Summary.Cell>
|
{InstanceRenderManager({
|
||||||
|
imex: (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
rome: (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mash.base).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
promanager: "USE_ROME"
|
||||||
|
})}
|
||||||
</Table.Summary.Row>
|
</Table.Summary.Row>
|
||||||
<Table.Summary.Row>
|
<Table.Summary.Row>
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Cell>
|
||||||
@@ -159,6 +246,16 @@ export default function JobTotalsTableLabor({ job }) {
|
|||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
<Table.Summary.Cell />
|
<Table.Summary.Cell />
|
||||||
<Table.Summary.Cell />
|
<Table.Summary.Cell />
|
||||||
|
{InstanceRenderManager({
|
||||||
|
imex: null,
|
||||||
|
rome: (
|
||||||
|
<>
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
<Table.Summary.Cell />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
promanager: "USE_ROME"
|
||||||
|
})}
|
||||||
<Table.Summary.Cell align="right">
|
<Table.Summary.Cell align="right">
|
||||||
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
|
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
|
||||||
</Table.Summary.Cell>
|
</Table.Summary.Cell>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Button, Space, notification } from "antd";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DELETE_DELIVERY_CHECKLIST, DELETE_INTAKE_CHECKLIST } from "../../graphql/jobs.queries";
|
import { DELETE_DELIVERY_CHECKLIST, DELETE_INTAKE_CHECKLIST } from "../../graphql/jobs.queries";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
export default function JobAdminDeleteIntake({ job }) {
|
export default function JobAdminDeleteIntake({ job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -47,16 +48,22 @@ export default function JobAdminDeleteIntake({ job }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const InstanceRender = InstanceRenderManager({
|
||||||
|
imex: true,
|
||||||
|
rome: "USE_IMEX",
|
||||||
|
promanager: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return InstanceRender ? (
|
||||||
<>
|
<>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button loading={loading} onClick={handleDelete} disabled={!job.intakechecklist}>
|
<Button loading={loading} onClick={handleDelete} disabled={!job.intakechecklist}>
|
||||||
{t("jobs.labels.deleteintake")}
|
{t("jobs.labels.deleteintake")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverychecklist}>
|
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverchecklist}>
|
||||||
{t("jobs.labels.deletedelivery")}
|
{t("jobs.labels.deletedelivery")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</>
|
</>
|
||||||
);
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|||||||
import { Col, Row, notification } from "antd";
|
import { Col, Row, notification } from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -24,6 +23,8 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import confirmDialog from "../../utils/asyncConfirm";
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||||
@@ -32,7 +33,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
|||||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -580,12 +580,13 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
|||||||
InstanceRenderManager({
|
InstanceRenderManager({
|
||||||
executeFunction: true,
|
executeFunction: true,
|
||||||
args: [],
|
args: [],
|
||||||
promanager: () => {
|
rome: () => {
|
||||||
if (line.mod_lbr_ty === "LAET" || line.mod_lbr_ty === "LAUT") {
|
if (line.mod_lbr_ty === "LAET" || line.mod_lbr_ty === "LAUT") {
|
||||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||||
line.mod_lbr_ty = "LAR";
|
line.mod_lbr_ty = "LAR";
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
promanager: "USE_ROME"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col {...lossColDamage}>
|
<Col {...lossColDamage}>
|
||||||
{job.area_of_damage ? (
|
{job.area_of_damage ? (
|
||||||
<Car dmg1={job.area_of_damage.impact1} dmg2={job.area_of_damage.impact2} />
|
<Car
|
||||||
|
dmg1={job.area_of_damage.impact1 && job.area_of_damage.impact1.padStart(2, "0")}
|
||||||
|
dmg2={job.area_of_damage.impact2 && job.area_of_damage.impact2.padStart(2, "0")}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
t("jobs.errors.nodamage")
|
t("jobs.errors.nodamage")
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -8,27 +11,24 @@ import { Link, useNavigate } from "react-router-dom";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||||
|
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||||
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
|
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
|
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
import axios from "axios";
|
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
|
||||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
|
||||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
||||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -83,6 +83,13 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
modal: "timeTicketTask"
|
modal: "timeTicketTask"
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
setTaskUpsertContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "taskUpsert"
|
||||||
|
})
|
||||||
|
),
|
||||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
setMessage: (text) => dispatch(setMessage(text))
|
setMessage: (text) => dispatch(setMessage(text))
|
||||||
@@ -104,7 +111,8 @@ export function JobsDetailHeaderActions({
|
|||||||
setEmailOptions,
|
setEmailOptions,
|
||||||
openChatByPhone,
|
openChatByPhone,
|
||||||
setMessage,
|
setMessage,
|
||||||
setTimeTicketTaskContext
|
setTimeTicketTaskContext,
|
||||||
|
setTaskUpsertContext
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -633,6 +641,7 @@ export function JobsDetailHeaderActions({
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
key: "schedule",
|
key: "schedule",
|
||||||
|
id: "job-actions-schedule",
|
||||||
disabled: !jobInPreProduction || !job.converted || jobRO,
|
disabled: !jobInPreProduction || !job.converted || jobRO,
|
||||||
label: t("jobs.actions.schedule"),
|
label: t("jobs.actions.schedule"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -649,6 +658,7 @@ export function JobsDetailHeaderActions({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "cancelallappointments",
|
key: "cancelallappointments",
|
||||||
|
id: "job-actions-cancelallappointments",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
|
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
|
||||||
return;
|
return;
|
||||||
@@ -662,6 +672,7 @@ export function JobsDetailHeaderActions({
|
|||||||
imex: [
|
imex: [
|
||||||
{
|
{
|
||||||
key: "intake",
|
key: "intake",
|
||||||
|
id: "job-actions-intake",
|
||||||
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
||||||
label:
|
label:
|
||||||
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
||||||
@@ -672,6 +683,7 @@ export function JobsDetailHeaderActions({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "deliver",
|
key: "deliver",
|
||||||
|
id: "job-actions-deliver",
|
||||||
disabled: !jobInProduction || jobRO,
|
disabled: !jobInProduction || jobRO,
|
||||||
label: !jobInProduction ? (
|
label: !jobInProduction ? (
|
||||||
t("jobs.actions.deliver")
|
t("jobs.actions.deliver")
|
||||||
@@ -681,6 +693,7 @@ export function JobsDetailHeaderActions({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "checklist",
|
key: "checklist",
|
||||||
|
id: "job-actions-checklist",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
|
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
|
||||||
}
|
}
|
||||||
@@ -689,6 +702,7 @@ export function JobsDetailHeaderActions({
|
|||||||
promanager: [
|
promanager: [
|
||||||
{
|
{
|
||||||
key: "toggleproduction",
|
key: "toggleproduction",
|
||||||
|
id: "job-actions-toggleproduction",
|
||||||
disabled: !job.converted || jobRO,
|
disabled: !job.converted || jobRO,
|
||||||
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
|
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
|
||||||
}
|
}
|
||||||
@@ -702,6 +716,7 @@ export function JobsDetailHeaderActions({
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: "entertimetickets",
|
key: "entertimetickets",
|
||||||
|
id: "job-actions-entertimetickets",
|
||||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||||
label: t("timetickets.actions.enter"),
|
label: t("timetickets.actions.enter"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -725,6 +740,7 @@ export function JobsDetailHeaderActions({
|
|||||||
if (bodyshop.md_tasks_presets.enable_tasks) {
|
if (bodyshop.md_tasks_presets.enable_tasks) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "claimtimetickettasks",
|
key: "claimtimetickettasks",
|
||||||
|
id: "job-actions-claimtimetickettasks",
|
||||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setTimeTicketTaskContext({
|
setTimeTicketTaskContext({
|
||||||
@@ -738,6 +754,7 @@ export function JobsDetailHeaderActions({
|
|||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "enterpayments",
|
key: "enterpayments",
|
||||||
|
id: "job-actions-enterpayments",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("menus.header.enterpayment"),
|
label: t("menus.header.enterpayment"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -753,22 +770,24 @@ export function JobsDetailHeaderActions({
|
|||||||
if (ImEXPay.treatment === "on") {
|
if (ImEXPay.treatment === "on") {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "entercardpayments",
|
key: "entercardpayments",
|
||||||
|
id: "job-actions-entercardpayments",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("menus.header.entercardpayment"),
|
label: t("menus.header.entercardpayment"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
logImEXEvent("job_header_enter_card_payment");
|
logImEXEvent("job_header_enter_card_payment");
|
||||||
|
|
||||||
setCardPaymentContext({
|
setCardPaymentContext({
|
||||||
actions: {},
|
actions: { refetch },
|
||||||
context: { jobid: job.id }
|
context: { jobid: job.id }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HasFeatureAccess({ featureName: "courtesycars" })) {
|
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "cccontract",
|
key: "cccontract",
|
||||||
|
id: "job-actions-cccontract",
|
||||||
disabled: jobRO || !job.converted,
|
disabled: jobRO || !job.converted,
|
||||||
label: (
|
label: (
|
||||||
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
|
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
|
||||||
@@ -778,16 +797,29 @@ export function JobsDetailHeaderActions({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menuItems.push({
|
||||||
|
key: "createtask",
|
||||||
|
id: "job-actions-createtask",
|
||||||
|
label: t("menus.header.create_task"),
|
||||||
|
onClick: () =>
|
||||||
|
setTaskUpsertContext({
|
||||||
|
actions: {},
|
||||||
|
context: { jobid: job.id }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
job.inproduction
|
job.inproduction
|
||||||
? {
|
? {
|
||||||
key: "removefromproduction",
|
key: "removefromproduction",
|
||||||
|
id: "job-actions-removefromproduction",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("jobs.actions.removefromproduction"),
|
label: t("jobs.actions.removefromproduction"),
|
||||||
onClick: () => AddToProduction(client, job.id, refetch, true)
|
onClick: () => AddToProduction(client, job.id, refetch, true)
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
key: "addtoproduction",
|
key: "addtoproduction",
|
||||||
|
id: "job-actions-addtoproduction",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("jobs.actions.addtoproduction"),
|
label: t("jobs.actions.addtoproduction"),
|
||||||
onClick: () => AddToProduction(client, job.id, refetch)
|
onClick: () => AddToProduction(client, job.id, refetch)
|
||||||
@@ -797,12 +829,14 @@ export function JobsDetailHeaderActions({
|
|||||||
menuItems.push(
|
menuItems.push(
|
||||||
{
|
{
|
||||||
key: "togglesuspend",
|
key: "togglesuspend",
|
||||||
|
id: "job-actions-togglesuspend",
|
||||||
onClick: handleSuspend,
|
onClick: handleSuspend,
|
||||||
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
|
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "toggleAlert",
|
key: "toggleAlert",
|
||||||
onClick: handleAlertToggle,
|
onClick: handleAlertToggle,
|
||||||
|
id: "job-actions-togglealert",
|
||||||
label:
|
label:
|
||||||
job.production_vars && job.production_vars.alert
|
job.production_vars && job.production_vars.alert
|
||||||
? t("production.labels.alertoff")
|
? t("production.labels.alertoff")
|
||||||
@@ -814,6 +848,7 @@ export function JobsDetailHeaderActions({
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: "duplicate",
|
key: "duplicate",
|
||||||
|
id: "job-actions-duplicate",
|
||||||
label: (
|
label: (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
title={t("jobs.labels.duplicateconfirm")}
|
||||||
@@ -829,6 +864,7 @@ export function JobsDetailHeaderActions({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "duplicatenolines",
|
key: "duplicatenolines",
|
||||||
|
id: "job-actions-duplicatenolines",
|
||||||
label: (
|
label: (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
title={t("jobs.labels.duplicateconfirm")}
|
||||||
@@ -852,6 +888,7 @@ export function JobsDetailHeaderActions({
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
key: "postbills",
|
key: "postbills",
|
||||||
|
id: "job-actions-postbills",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("jobs.actions.postbills"),
|
label: t("jobs.actions.postbills"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -870,6 +907,7 @@ export function JobsDetailHeaderActions({
|
|||||||
|
|
||||||
{
|
{
|
||||||
key: "addtopartsqueue",
|
key: "addtopartsqueue",
|
||||||
|
id: "job-actions-addtopartsqueue",
|
||||||
disabled: !job.converted || !jobInProduction || jobRO,
|
disabled: !job.converted || !jobInProduction || jobRO,
|
||||||
label: t("jobs.actions.addtopartsqueue"),
|
label: t("jobs.actions.addtopartsqueue"),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
@@ -895,6 +933,7 @@ export function JobsDetailHeaderActions({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "closejob",
|
key: "closejob",
|
||||||
|
id: "job-actions-closejob",
|
||||||
disabled: !jobInPostProduction,
|
disabled: !jobInPostProduction,
|
||||||
label: !jobInPostProduction ? (
|
label: !jobInPostProduction ? (
|
||||||
t("menus.jobsactions.closejob")
|
t("menus.jobsactions.closejob")
|
||||||
@@ -910,6 +949,7 @@ export function JobsDetailHeaderActions({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "admin",
|
key: "admin",
|
||||||
|
id: "job-actions-admin",
|
||||||
label: (
|
label: (
|
||||||
<Link
|
<Link
|
||||||
to={{
|
to={{
|
||||||
@@ -931,6 +971,7 @@ export function JobsDetailHeaderActions({
|
|||||||
) {
|
) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "exportcustdata",
|
key: "exportcustdata",
|
||||||
|
id: "job-actions-exportcustdata",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("jobs.actions.exportcustdata"),
|
label: t("jobs.actions.exportcustdata"),
|
||||||
onClick: handleExportCustData
|
onClick: handleExportCustData
|
||||||
@@ -941,18 +982,21 @@ export function JobsDetailHeaderActions({
|
|||||||
const children = [
|
const children = [
|
||||||
{
|
{
|
||||||
key: "email",
|
key: "email",
|
||||||
|
id: "job-actions-email",
|
||||||
disabled: !!!job.ownr_ea,
|
disabled: !!!job.ownr_ea,
|
||||||
label: t("general.labels.email"),
|
label: t("general.labels.email"),
|
||||||
onClick: handleCreateCsi
|
onClick: handleCreateCsi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "text",
|
key: "text",
|
||||||
|
id: "job-actions-text",
|
||||||
disabled: !!!job.ownr_ph1,
|
disabled: !!!job.ownr_ph1,
|
||||||
label: t("general.labels.text"),
|
label: t("general.labels.text"),
|
||||||
onClick: handleCreateCsi
|
onClick: handleCreateCsi
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "generate",
|
key: "generate",
|
||||||
|
id: "job-actions-generate",
|
||||||
disabled: job.csiinvites && job.csiinvites.length > 0,
|
disabled: job.csiinvites && job.csiinvites.length > 0,
|
||||||
label: t("jobs.actions.generatecsi"),
|
label: t("jobs.actions.generatecsi"),
|
||||||
onClick: handleCreateCsi
|
onClick: handleCreateCsi
|
||||||
@@ -986,6 +1030,7 @@ export function JobsDetailHeaderActions({
|
|||||||
}
|
}
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "sendcsi",
|
key: "sendcsi",
|
||||||
|
id: "job-actions-sendcsi",
|
||||||
label: t("jobs.actions.sendcsi"),
|
label: t("jobs.actions.sendcsi"),
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
children
|
children
|
||||||
@@ -994,6 +1039,7 @@ export function JobsDetailHeaderActions({
|
|||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "jobcosting",
|
key: "jobcosting",
|
||||||
|
id: "job-actions-jobcosting",
|
||||||
disabled: !job.converted,
|
disabled: !job.converted,
|
||||||
label: t("jobs.labels.jobcosting"),
|
label: t("jobs.labels.jobcosting"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -1011,6 +1057,7 @@ export function JobsDetailHeaderActions({
|
|||||||
if (job && !job.converted) {
|
if (job && !job.converted) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "deletejob",
|
key: "deletejob",
|
||||||
|
id: "job-actions-deletejob",
|
||||||
label: (
|
label: (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.deleteconfirm")}
|
title={t("jobs.labels.deleteconfirm")}
|
||||||
@@ -1027,6 +1074,7 @@ export function JobsDetailHeaderActions({
|
|||||||
|
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "manualevent",
|
key: "manualevent",
|
||||||
|
id: "job-actions-manualevent",
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
setVisibility(true);
|
setVisibility(true);
|
||||||
},
|
},
|
||||||
@@ -1036,6 +1084,7 @@ export function JobsDetailHeaderActions({
|
|||||||
if (!jobRO && job.converted) {
|
if (!jobRO && job.converted) {
|
||||||
menuItems.push({
|
menuItems.push({
|
||||||
key: "voidjob",
|
key: "voidjob",
|
||||||
|
id: "job-actions-voidjob",
|
||||||
label: (
|
label: (
|
||||||
<RbacWrapper action="jobs:void" noauth>
|
<RbacWrapper action="jobs:void" noauth>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
|||||||
@@ -1,55 +1,13 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
|
||||||
import queryString from "query-string";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
|
||||||
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
|
||||||
import JobsDetailPliComponent from "./jobs-detail-pli.component";
|
import JobsDetailPliComponent from "./jobs-detail-pli.component";
|
||||||
|
|
||||||
export default function JobsDetailPliContainer({ job }) {
|
export default function JobsDetailPliContainer({
|
||||||
const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
|
job,
|
||||||
variables: { jobid: job.id },
|
billsQuery,
|
||||||
fetchPolicy: "network-only",
|
handleBillOnRowClick,
|
||||||
nextFetchPolicy: "network-only"
|
handlePartsOrderOnRowClick,
|
||||||
});
|
handlePartsDispatchOnRowClick
|
||||||
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
const history = useNavigate();
|
|
||||||
|
|
||||||
const handleBillOnRowClick = (record) => {
|
|
||||||
if (record) {
|
|
||||||
if (record.id) {
|
|
||||||
search.billid = record.id;
|
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete search.billid;
|
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePartsOrderOnRowClick = (record) => {
|
|
||||||
if (record) {
|
|
||||||
if (record.id) {
|
|
||||||
search.partsorderid = record.id;
|
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete search.partsorderid;
|
|
||||||
history({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePartsDispatchOnRowClick = (record) => {
|
|
||||||
if (record) {
|
|
||||||
if (record.id) {
|
|
||||||
search.partsdispatchid = record.id;
|
|
||||||
history.push({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete search.partsdispatchid;
|
|
||||||
history.push({ search: queryString.stringify(search) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<JobsDetailPliComponent
|
<JobsDetailPliComponent
|
||||||
job={job}
|
job={job}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Collapse, Form, Switch } from "antd";
|
import { Collapse, Form, InputNumber, Switch } 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";
|
||||||
@@ -17,6 +17,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
<Collapse defaultActiveKey={expanded && "rates"}>
|
<Collapse defaultActiveKey={expanded && "rates"}>
|
||||||
<Collapse.Panel forceRender header={t("jobs.labels.cieca_pfl")} key="cieca_pfl">
|
<Collapse.Panel forceRender header={t("jobs.labels.cieca_pfl")} key="cieca_pfl">
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAB", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAB", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAB", "lbr_tax_in"]}
|
||||||
@@ -24,6 +27,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAB", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAB", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAB", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAB", "lbr_tx_in1"]}
|
||||||
@@ -61,6 +82,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAD")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAD")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAD", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAD", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAD", "lbr_tax_in"]}
|
||||||
@@ -68,6 +92,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAD", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAD", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAD", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAD", "lbr_tx_in1"]}
|
||||||
@@ -105,6 +147,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAE")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAE")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAE", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAE", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAE", "lbr_tax_in"]}
|
||||||
@@ -112,6 +157,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAE", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAE", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAE", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAE", "lbr_tx_in1"]}
|
||||||
@@ -149,6 +212,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAF")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAF")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAF", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAF", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAF", "lbr_tax_in"]}
|
||||||
@@ -156,6 +222,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAF", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAF", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAF", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAF", "lbr_tx_in1"]}
|
||||||
@@ -193,6 +277,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAG")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAG")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAG", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAG", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAG", "lbr_tax_in"]}
|
||||||
@@ -200,6 +287,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAG", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAG", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAG", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAG", "lbr_tx_in1"]}
|
||||||
@@ -237,6 +342,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAM")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAM")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAM", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAM", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAM", "lbr_tax_in"]}
|
||||||
@@ -244,6 +352,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAM", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAM", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAM", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAM", "lbr_tx_in1"]}
|
||||||
@@ -281,6 +407,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAR")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAR")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAR", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAR", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAR", "lbr_tax_in"]}
|
||||||
@@ -288,6 +417,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAR", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAR", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAR", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAR", "lbr_tx_in1"]}
|
||||||
@@ -325,6 +472,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAS")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAS")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAS", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAS", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAS", "lbr_tax_in"]}
|
||||||
@@ -332,6 +482,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAS", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAS", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAS", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAS", "lbr_tx_in1"]}
|
||||||
@@ -369,6 +537,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAU")}>
|
<LayoutFormRow header={t("joblines.fields.lbr_types.LAU")}>
|
||||||
|
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAU", "lbr_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||||
name={["cieca_pfl", "LAU", "lbr_tax_in"]}
|
name={["cieca_pfl", "LAU", "lbr_tax_in"]}
|
||||||
@@ -376,6 +547,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
|||||||
>
|
>
|
||||||
<Switch disabled={jobRO} />
|
<Switch disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||||
|
name={["cieca_pfl", "LAU", "lbr_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["cieca_pfl", "LAU", "lbr_tax_in"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||||
name={["cieca_pfl", "LAU", "lbr_tx_in1"]}
|
name={["cieca_pfl", "LAU", "lbr_tx_in1"]}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
|||||||
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MAPA", "cal_opcode"]}>
|
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MAPA", "cal_opcode"]}>
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.tax_ind")}
|
label={t("jobs.fields.materials.tax_ind")}
|
||||||
name={["materials", "MAPA", "tax_ind"]}
|
name={["materials", "MAPA", "tax_ind"]}
|
||||||
@@ -31,6 +33,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.materials.mat_taxp")}
|
||||||
|
name={["materials", "MAPA", "mat_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["materials", "MAPA", "tax_ind"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||||
name={["materials", "MAPA", "mat_tx_in1"]}
|
name={["materials", "MAPA", "mat_tx_in1"]}
|
||||||
@@ -74,7 +94,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
|||||||
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MASH", "cal_opcode"]}>
|
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MASH", "cal_opcode"]}>
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
|
||||||
|
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.tax_ind")}
|
label={t("jobs.fields.materials.tax_ind")}
|
||||||
name={["materials", "MASH", "tax_ind"]}
|
name={["materials", "MASH", "tax_ind"]}
|
||||||
@@ -82,6 +104,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.materials.mat_taxp")}
|
||||||
|
name={["materials", "MASH", "mat_taxp"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: form.getFieldValue(["materials", "MASH", "tax_ind"])
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||||
name={["materials", "MASH", "mat_tx_in1"]}
|
name={["materials", "MASH", "mat_tx_in1"]}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
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({
|
||||||
@@ -988,18 +989,24 @@ export function JobsDetailRatesParts({ jobRO, expanded, required = true, form })
|
|||||||
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
|
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||||
|
<>
|
||||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>{" "}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_rt">
|
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_rt">
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
) : null}
|
||||||
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
|
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
|||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
||||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -315,34 +314,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
|||||||
title={t("titles.bc.jobs-active")}
|
title={t("titles.bc.jobs-active")}
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{InstanceRenderManager({
|
|
||||||
promanager: (
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
setJoyRideSteps([
|
|
||||||
{
|
|
||||||
target: "#active-jobs-list",
|
|
||||||
content: "This is where you will see all work coming in and currently here."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: "#header-jobs",
|
|
||||||
spotlightClicks: true,
|
|
||||||
disableOverlayClose: true,
|
|
||||||
content:
|
|
||||||
"The jobs menu lets you access additional pages to see more information. You can import new jobs, search all jobs, or manage your current production."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: "#header-jobs-available",
|
|
||||||
content: "You can find jobs to import here."
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Start Walk Through
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Button onClick={() => refetch()}>
|
<Button onClick={() => refetch()}>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||||
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -75,7 +76,18 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
|||||||
})),
|
})),
|
||||||
onFilter: (value, record) => value.includes(record.status)
|
onFilter: (value, record) => value.includes(record.status)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.actual_completion"),
|
||||||
|
dataIndex: "actual_completion",
|
||||||
|
key: "actual_completion",
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "actual_completion" &&
|
||||||
|
state.sortedInfo.order,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.clm_total"),
|
title: t("jobs.fields.clm_total"),
|
||||||
dataIndex: "clm_total",
|
dataIndex: "clm_total",
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
|
||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
|
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
|
||||||
|
|
||||||
|
import queryString from "query-string";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FaTasks } from "react-icons/fa";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||||
|
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||||
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||||
|
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||||
|
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||||
|
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||||
|
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||||
|
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
jobRO: selectJobReadOnly,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBillEnterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "billEnter"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
setPartsReceiveContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "partsReceive"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function PartsOrderListTableDrawerComponent({
|
||||||
|
setBillEnterContext,
|
||||||
|
bodyshop,
|
||||||
|
jobRO,
|
||||||
|
job,
|
||||||
|
billsQuery,
|
||||||
|
handleOnRowClick,
|
||||||
|
setPartsReceiveContext,
|
||||||
|
setTaskUpsertContext
|
||||||
|
}) {
|
||||||
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
|
.filter((screen) => !!screen[1])
|
||||||
|
.slice(-1)[0];
|
||||||
|
|
||||||
|
const bpoints = {
|
||||||
|
xs: "100%",
|
||||||
|
sm: "100%",
|
||||||
|
md: "100%",
|
||||||
|
lg: "75%",
|
||||||
|
xl: "75%",
|
||||||
|
xxl: "65%"
|
||||||
|
};
|
||||||
|
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||||
|
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||||
|
const Templates = TemplateList("partsorder", { job });
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [returnfrombill, setReturnFromBill] = useState();
|
||||||
|
const [billData, setBillData] = useState();
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
|
const selectedpartsorder = search.partsorderid;
|
||||||
|
|
||||||
|
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||||
|
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||||
|
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||||
|
const { refetch } = billsQuery;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (returnfrombill === null) {
|
||||||
|
setBillData(null);
|
||||||
|
} else {
|
||||||
|
const fetchData = async () => {
|
||||||
|
const result = await billQuery({
|
||||||
|
variables: { billid: returnfrombill }
|
||||||
|
});
|
||||||
|
setBillData(result.data);
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [returnfrombill, billQuery]);
|
||||||
|
|
||||||
|
const recordActions = (record, showView = false) => (
|
||||||
|
<Space direction="horizontal" wrap>
|
||||||
|
{showView && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (record.returnfrombill) {
|
||||||
|
setReturnFromBill(record.returnfrombill);
|
||||||
|
} else {
|
||||||
|
setReturnFromBill(null);
|
||||||
|
}
|
||||||
|
handleOnRowClick(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EyeFilled />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("parts_order_receive_bill");
|
||||||
|
setPartsReceiveContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
jobId: job.id,
|
||||||
|
job: job,
|
||||||
|
partsorderlines: record.parts_order_lines.map((pol) => {
|
||||||
|
return {
|
||||||
|
joblineid: pol.job_line_id,
|
||||||
|
id: pol.id,
|
||||||
|
line_desc: pol.line_desc,
|
||||||
|
quantity: pol.quantity,
|
||||||
|
act_price: pol.act_price,
|
||||||
|
oem_partno: pol.oem_partno
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("parts_orders.actions.receive")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title={t("tasks.buttons.create")}
|
||||||
|
onClick={() => {
|
||||||
|
setTaskUpsertContext({
|
||||||
|
context: {
|
||||||
|
jobid: job.id,
|
||||||
|
partsorderid: record.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaTasks />
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title={t("parts_orders.labels.confirmdelete")}
|
||||||
|
disabled={jobRO}
|
||||||
|
onConfirm={async () => {
|
||||||
|
//Delete the parts return.!
|
||||||
|
|
||||||
|
await deletePartsOrder({
|
||||||
|
variables: { partsOrderId: record.id },
|
||||||
|
update(cache) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
parts_orders(existingPartsOrders, { readField }) {
|
||||||
|
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button disabled={jobRO}>
|
||||||
|
<DeleteFilled />
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
|
||||||
|
<Button
|
||||||
|
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
|
||||||
|
onClick={() => {
|
||||||
|
logImEXEvent("parts_order_receive_bill");
|
||||||
|
|
||||||
|
setBillEnterContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
job: job,
|
||||||
|
bill: {
|
||||||
|
vendorid: record.vendor.id,
|
||||||
|
is_credit_memo: record.return,
|
||||||
|
billlines: record.parts_order_lines.map((pol) => {
|
||||||
|
return {
|
||||||
|
joblineid: pol.job_line_id || "noline",
|
||||||
|
line_desc: pol.line_desc,
|
||||||
|
quantity: pol.quantity,
|
||||||
|
|
||||||
|
actual_price: pol.act_price,
|
||||||
|
|
||||||
|
cost_center: pol.jobline?.part_type
|
||||||
|
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||||
|
? pol.jobline.part_type !== "PAE"
|
||||||
|
? pol.jobline.part_type
|
||||||
|
: null
|
||||||
|
: responsibilityCenters.defaults &&
|
||||||
|
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("parts_orders.actions.receivebill")}
|
||||||
|
</Button>
|
||||||
|
</FeatureWrapperComponent>
|
||||||
|
<PrintWrapper
|
||||||
|
templateObject={{
|
||||||
|
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
|
||||||
|
variables: { id: record.id }
|
||||||
|
}}
|
||||||
|
messageObject={{
|
||||||
|
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
|
||||||
|
to: record.vendor.email
|
||||||
|
}}
|
||||||
|
id={job.id}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||||
|
|
||||||
|
const rowExpander = (record) => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.line_desc"),
|
||||||
|
dataIndex: "line_desc",
|
||||||
|
key: "line_desc",
|
||||||
|
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.quantity"),
|
||||||
|
dataIndex: "quantity",
|
||||||
|
key: "quantity",
|
||||||
|
sorter: (a, b) => a.quantity - b.quantity,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.act_price"),
|
||||||
|
dataIndex: "act_price",
|
||||||
|
key: "act_price",
|
||||||
|
sorter: (a, b) => a.act_price - b.act_price,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||||
|
},
|
||||||
|
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.cost"),
|
||||||
|
dataIndex: "cost",
|
||||||
|
key: "cost",
|
||||||
|
sorter: (a, b) => a.cost - b.cost,
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.part_type"),
|
||||||
|
dataIndex: "part_type",
|
||||||
|
key: "part_type",
|
||||||
|
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.oem_partno"),
|
||||||
|
dataIndex: "oem_partno",
|
||||||
|
key: "oem_partno",
|
||||||
|
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||||
|
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.line_remarks"),
|
||||||
|
dataIndex: "line_remarks",
|
||||||
|
key: "line_remarks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status"
|
||||||
|
},
|
||||||
|
|
||||||
|
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.cm_received"),
|
||||||
|
dataIndex: "cm_received",
|
||||||
|
key: "cm_received",
|
||||||
|
render: (text, record) => (
|
||||||
|
<PartsOrderCmReceived
|
||||||
|
orderLineId={record.id}
|
||||||
|
checked={record.cm_received}
|
||||||
|
partsorderid={selectedPartsOrderRecord.id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.backordered_on"),
|
||||||
|
dataIndex: "backordered_on",
|
||||||
|
key: "backordered_on",
|
||||||
|
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.backordered_eta"),
|
||||||
|
dataIndex: "backordered_eta",
|
||||||
|
key: "backordered_eta",
|
||||||
|
render: (text, record) => (
|
||||||
|
<PartsOrderBackorderEta
|
||||||
|
backordered_eta={record.backordered_eta}
|
||||||
|
disabled={jobRO}
|
||||||
|
partsOrderStatus={record.status}
|
||||||
|
partsLineId={record.id}
|
||||||
|
jobLineId={record.job_line_id}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
render: (text, record) => (
|
||||||
|
<Space wrap>
|
||||||
|
<PartsOrderDeleteLine
|
||||||
|
disabled={jobRO}
|
||||||
|
partsOrderStatus={record.status}
|
||||||
|
partsLineId={record.id}
|
||||||
|
partsOrderId={selectedpartsorder}
|
||||||
|
jobLineId={record.job_line_id}
|
||||||
|
/>
|
||||||
|
<PartsOrderLineBackorderButton
|
||||||
|
disabled={jobRO}
|
||||||
|
partsOrderStatus={record.status}
|
||||||
|
partsLineId={record.id}
|
||||||
|
jobLineId={record.job_line_id}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title={
|
||||||
|
billData
|
||||||
|
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||||
|
: `${record.vendor.name} - ${record.order_number}`
|
||||||
|
}
|
||||||
|
extra={recordActions(record)}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
scroll={{
|
||||||
|
x: true //y: "50rem"
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={record.parts_order_lines}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||||
|
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||||
|
</DataLabel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PartsReceiveModalContainer />
|
||||||
|
<Drawer
|
||||||
|
placement="right"
|
||||||
|
onClose={() => handleOnRowClick(null)}
|
||||||
|
open={selectedpartsorder}
|
||||||
|
closable
|
||||||
|
width={drawerPercentage}
|
||||||
|
>
|
||||||
|
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderListTableDrawerComponent);
|
||||||
@@ -1,32 +1,23 @@
|
|||||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
|
||||||
|
|
||||||
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 { FaTasks } from "react-icons/fa";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
|
||||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
|
||||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
|
||||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
||||||
import { FaTasks } from "react-icons/fa";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -61,19 +52,6 @@ export function PartsOrderListTableComponent({
|
|||||||
setPartsReceiveContext,
|
setPartsReceiveContext,
|
||||||
setTaskUpsertContext
|
setTaskUpsertContext
|
||||||
}) {
|
}) {
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
|
||||||
.filter((screen) => !!screen[1])
|
|
||||||
.slice(-1)[0];
|
|
||||||
|
|
||||||
const bpoints = {
|
|
||||||
xs: "100%",
|
|
||||||
sm: "100%",
|
|
||||||
md: "100%",
|
|
||||||
lg: "75%",
|
|
||||||
xl: "75%",
|
|
||||||
xxl: "65%"
|
|
||||||
};
|
|
||||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
|
||||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||||
const Templates = TemplateList("partsorder", { job });
|
const Templates = TemplateList("partsorder", { job });
|
||||||
|
|
||||||
@@ -81,10 +59,8 @@ export function PartsOrderListTableComponent({
|
|||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {}
|
sortedInfo: {}
|
||||||
});
|
});
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
const selectedpartsorder = search.partsorderid;
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||||
|
|
||||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||||
@@ -93,7 +69,11 @@ export function PartsOrderListTableComponent({
|
|||||||
const recordActions = (record, showView = false) => (
|
const recordActions = (record, showView = false) => (
|
||||||
<Space direction="horizontal" wrap>
|
<Space direction="horizontal" wrap>
|
||||||
{showView && (
|
{showView && (
|
||||||
<Button onClick={() => handleOnRowClick(record)}>
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<EyeFilled />
|
<EyeFilled />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -175,7 +155,7 @@ export function PartsOrderListTableComponent({
|
|||||||
is_credit_memo: record.return,
|
is_credit_memo: record.return,
|
||||||
billlines: record.parts_order_lines.map((pol) => {
|
billlines: record.parts_order_lines.map((pol) => {
|
||||||
return {
|
return {
|
||||||
joblineid: pol.job_line_id,
|
joblineid: pol.job_line_id || "noline",
|
||||||
line_desc: pol.line_desc,
|
line_desc: pol.line_desc,
|
||||||
quantity: pol.quantity,
|
quantity: pol.quantity,
|
||||||
|
|
||||||
@@ -270,147 +250,6 @@ export function PartsOrderListTableComponent({
|
|||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
|
||||||
|
|
||||||
const rowExpander = (record) => {
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.line_desc"),
|
|
||||||
dataIndex: "line_desc",
|
|
||||||
key: "line_desc",
|
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
|
||||||
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.quantity"),
|
|
||||||
dataIndex: "quantity",
|
|
||||||
key: "quantity",
|
|
||||||
sorter: (a, b) => a.quantity - b.quantity,
|
|
||||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.act_price"),
|
|
||||||
dataIndex: "act_price",
|
|
||||||
key: "act_price",
|
|
||||||
sorter: (a, b) => a.act_price - b.act_price,
|
|
||||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
|
||||||
},
|
|
||||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.cost"),
|
|
||||||
dataIndex: "cost",
|
|
||||||
key: "cost",
|
|
||||||
sorter: (a, b) => a.cost - b.cost,
|
|
||||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
|
||||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.part_type"),
|
|
||||||
dataIndex: "part_type",
|
|
||||||
key: "part_type",
|
|
||||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.oem_partno"),
|
|
||||||
dataIndex: "oem_partno",
|
|
||||||
key: "oem_partno",
|
|
||||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
|
||||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.line_remarks"),
|
|
||||||
dataIndex: "line_remarks",
|
|
||||||
key: "line_remarks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.status"),
|
|
||||||
dataIndex: "status",
|
|
||||||
key: "status"
|
|
||||||
},
|
|
||||||
|
|
||||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.cm_received"),
|
|
||||||
dataIndex: "cm_received",
|
|
||||||
key: "cm_received",
|
|
||||||
render: (text, record) => (
|
|
||||||
<PartsOrderCmReceived
|
|
||||||
orderLineId={record.id}
|
|
||||||
checked={record.cm_received}
|
|
||||||
partsorderid={selectedPartsOrderRecord.id}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.backordered_on"),
|
|
||||||
dataIndex: "backordered_on",
|
|
||||||
key: "backordered_on",
|
|
||||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("parts_orders.fields.backordered_eta"),
|
|
||||||
dataIndex: "backordered_eta",
|
|
||||||
key: "backordered_eta",
|
|
||||||
render: (text, record) => (
|
|
||||||
<PartsOrderBackorderEta
|
|
||||||
backordered_eta={record.backordered_eta}
|
|
||||||
disabled={jobRO}
|
|
||||||
partsOrderStatus={record.status}
|
|
||||||
partsLineId={record.id}
|
|
||||||
jobLineId={record.job_line_id}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
title: t("general.labels.actions"),
|
|
||||||
dataIndex: "actions",
|
|
||||||
key: "actions",
|
|
||||||
render: (text, record) => (
|
|
||||||
<Space wrap>
|
|
||||||
<PartsOrderDeleteLine
|
|
||||||
disabled={jobRO}
|
|
||||||
partsOrderStatus={record.status}
|
|
||||||
partsLineId={record.id}
|
|
||||||
partsOrderId={selectedpartsorder}
|
|
||||||
jobLineId={record.job_line_id}
|
|
||||||
/>
|
|
||||||
<PartsOrderLineBackorderButton
|
|
||||||
disabled={jobRO}
|
|
||||||
partsOrderStatus={record.status}
|
|
||||||
partsLineId={record.id}
|
|
||||||
jobLineId={record.job_line_id}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} extra={recordActions(record)} />
|
|
||||||
<Table
|
|
||||||
scroll={{
|
|
||||||
x: true //y: "50rem"
|
|
||||||
}}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
|
||||||
dataSource={record.parts_order_lines}
|
|
||||||
/>
|
|
||||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
|
||||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
|
||||||
</DataLabel>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredPartsOrders = parts_orders
|
const filteredPartsOrders = parts_orders
|
||||||
? searchText === ""
|
? searchText === ""
|
||||||
? parts_orders
|
? parts_orders
|
||||||
@@ -441,15 +280,13 @@ export function PartsOrderListTableComponent({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PartsReceiveModalContainer />
|
<PartsReceiveModalContainer />
|
||||||
<Drawer
|
<PartsOrderDrawer
|
||||||
placement="right"
|
job={job}
|
||||||
onClose={() => handleOnRowClick(null)}
|
billsQuery={billsQuery}
|
||||||
open={selectedpartsorder}
|
handleOnRowClick={handleOnRowClick}
|
||||||
closable
|
setPartsReceiveContext={setPartsReceiveContext}
|
||||||
width={drawerPercentage}
|
setTaskUpsertContext={setTaskUpsertContext}
|
||||||
>
|
/>
|
||||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
|
||||||
</Drawer>
|
|
||||||
<Table
|
<Table
|
||||||
loading={billsQuery.loading}
|
loading={billsQuery.loading}
|
||||||
scroll={{
|
scroll={{
|
||||||
|
|||||||
@@ -119,8 +119,14 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Descriptions title={t("job_payments.titles.descriptions")} contentStyle={{ fontWeight: "600" }} column={4}>
|
<Descriptions
|
||||||
<Descriptions.Item label={t("job_payments.titles.payer")}>{record.payer}</Descriptions.Item>
|
title={t("job_payments.titles.descriptions")}
|
||||||
|
contentStyle={{ fontWeight: "600" }}
|
||||||
|
column={4}
|
||||||
|
>
|
||||||
|
<Descriptions.Item label={t("job_payments.titles.hint")}>
|
||||||
|
{payment_response?.response?.methodhint}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label={t("job_payments.titles.payername")}>
|
<Descriptions.Item label={t("job_payments.titles.payername")}>
|
||||||
{payment_response?.response?.nameOnCard ?? ""}
|
{payment_response?.response?.nameOnCard ?? ""}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
@@ -132,7 +138,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
|||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{record.transactionid}</Descriptions.Item>
|
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{record.transactionid}</Descriptions.Item>
|
||||||
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
|
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
|
||||||
{payment_response?.response?.paymentreferenceid ?? ""}
|
{payment_response?.ext_paymentid ?? ""}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>{record.type}</Descriptions.Item>
|
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>{record.type}</Descriptions.Item>
|
||||||
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>
|
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
|
|||||||
|
|
||||||
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{loading && <Spin />}
|
{loading && <Spin />}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
.imex-kanban-card {
|
|
||||||
padding: 0px !important;
|
|
||||||
|
|
||||||
.ant-card-body {
|
|
||||||
padding: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-card-head {
|
|
||||||
padding: 0rem 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
import {
|
|
||||||
BranchesOutlined,
|
|
||||||
CalendarOutlined,
|
|
||||||
DownloadOutlined,
|
|
||||||
EyeFilled,
|
|
||||||
PauseCircleOutlined
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
|
||||||
import React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
||||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
|
||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
|
||||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
|
||||||
import "./production-board-card.styles.scss";
|
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
|
||||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
|
||||||
|
|
||||||
const cardColor = (ssbuckets, totalHrs) => {
|
|
||||||
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
|
|
||||||
|
|
||||||
let color = { r: 255, g: 255, b: 255 };
|
|
||||||
|
|
||||||
if (bucket && bucket.color) {
|
|
||||||
color = bucket.color;
|
|
||||||
|
|
||||||
if (bucket.color.rgb) {
|
|
||||||
color = bucket.color.rgb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return color;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getContrastYIQ(bgColor) {
|
|
||||||
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
|
|
||||||
|
|
||||||
return yiq >= 128 ? "black" : "white";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
let employee_body, employee_prep, employee_refinish, employee_csr;
|
|
||||||
if (card.employee_body) {
|
|
||||||
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
|
|
||||||
}
|
|
||||||
if (card.employee_prep) {
|
|
||||||
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
|
|
||||||
}
|
|
||||||
if (card.employee_refinish) {
|
|
||||||
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
|
|
||||||
}
|
|
||||||
if (card.employee_csr) {
|
|
||||||
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
|
||||||
}
|
|
||||||
// if (card.employee_csr) {
|
|
||||||
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const pastDueAlert =
|
|
||||||
!!card.scheduled_completion &&
|
|
||||||
((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") ||
|
|
||||||
(dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon"));
|
|
||||||
|
|
||||||
const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
|
|
||||||
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className="react-kanban-card imex-kanban-card"
|
|
||||||
size="small"
|
|
||||||
style={{
|
|
||||||
backgroundColor:
|
|
||||||
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
|
||||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
|
|
||||||
}}
|
|
||||||
title={
|
|
||||||
<Space>
|
|
||||||
<ProductionAlert record={card} key="alert" />
|
|
||||||
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
|
||||||
{card.iouparent && (
|
|
||||||
<Tooltip title={t("jobs.labels.iou")}>
|
|
||||||
<BranchesOutlined style={{ color: "orangered" }} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<span style={{ fontWeight: "bolder" }}>
|
|
||||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
|
||||||
{card.ro_number || t("general.labels.na")}
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
extra={
|
|
||||||
<Link to={{ search: `?selected=${card.id}` }}>
|
|
||||||
<EyeFilled />
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Row>
|
|
||||||
{cardSettings && cardSettings.ownr_nm && (
|
|
||||||
<Col span={24}>
|
|
||||||
{cardSettings && cardSettings.compact ? (
|
|
||||||
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
|
|
||||||
) : (
|
|
||||||
<div className="ellipses">
|
|
||||||
<OwnerNameDisplay ownerObject={card} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
<Col span={24}>
|
|
||||||
<div className="ellipses">{`${card.v_model_yr || ""} ${
|
|
||||||
card.v_make_desc || ""
|
|
||||||
} ${card.v_model_desc || ""}`}</div>
|
|
||||||
</Col>
|
|
||||||
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
|
||||||
<div className="ellipses">{card.ins_co_nm || ""}</div>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{cardSettings && cardSettings.clm_no && card.clm_no && (
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
|
||||||
<div className="ellipses">{card.clm_no || ""}</div>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{cardSettings && cardSettings.employeeassignments && (
|
|
||||||
<Col span={24}>
|
|
||||||
<Row>
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
|
||||||
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
|
|
||||||
} ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`P: ${
|
|
||||||
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
|
|
||||||
}`}</Col>
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
|
||||||
employee_refinish
|
|
||||||
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
|
|
||||||
: ""
|
|
||||||
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
|
|
||||||
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
|
|
||||||
}`}</Col>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{/* {cardSettings && cardSettings.laborhrs && (
|
|
||||||
<Col span={24}>
|
|
||||||
<Row>
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
|
||||||
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
|
|
||||||
} hrs`}</Col>
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
|
||||||
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
|
|
||||||
} hrs`}</Col>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
)} */}
|
|
||||||
{cardSettings && cardSettings.actual_in && card.actual_in && (
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
|
||||||
<Space>
|
|
||||||
<DownloadOutlined />
|
|
||||||
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
|
|
||||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
|
||||||
<Space className={pastDueAlert}>
|
|
||||||
<CalendarOutlined />
|
|
||||||
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
|
|
||||||
</Space>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{cardSettings && cardSettings.ats && card.alt_transport && (
|
|
||||||
<Col span={12}>
|
|
||||||
<div>{card.alt_transport || ""}</div>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{cardSettings && cardSettings.sublets && (
|
|
||||||
<Col span={12}>
|
|
||||||
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{cardSettings && cardSettings.production_note && (
|
|
||||||
<Col span={24}>
|
|
||||||
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
{cardSettings && cardSettings.partsstatus && (
|
|
||||||
<Col span={24}>
|
|
||||||
<JobPartsQueueCount parts={card.joblines_status} />
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
import {
|
||||||
|
BranchesOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
EyeFilled,
|
||||||
|
PauseCircleOutlined
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
|
||||||
|
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||||
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
|
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||||
|
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
|
|
||||||
|
const cardColor = (ssbuckets, totalHrs) => {
|
||||||
|
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||||
|
return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContrastYIQ = (bgColor) =>
|
||||||
|
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
|
||||||
|
|
||||||
|
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
|
||||||
|
|
||||||
|
const EllipsesToolTip = React.memo(({ title, children, kiosk }) => {
|
||||||
|
if (kiosk || !title) {
|
||||||
|
return <div className="ellipses no-select">{children}</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip title={title}>
|
||||||
|
<div className="ellipses">{children}</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.ownr_nm && (
|
||||||
|
<Col span={24}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={metadata.ownr_ln || metadata.ownr_co_nm ? <OwnerNameDisplay ownerObject={metadata} /> : null}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{metadata.ownr_ln || metadata.ownr_co_nm ? (
|
||||||
|
cardSettings.compact ? (
|
||||||
|
`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`
|
||||||
|
) : (
|
||||||
|
<OwnerNameDisplay ownerObject={metadata} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ModelInfoToolTip = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.model_info && (
|
||||||
|
<Col span={24}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={
|
||||||
|
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc
|
||||||
|
? `${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc ? (
|
||||||
|
`${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const InsuranceCompanyToolTip = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.ins_co_nm && (
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip title={metadata.ins_co_nm || null} kiosk={cardSettings.kiosk}>
|
||||||
|
{metadata.ins_co_nm ? metadata.ins_co_nm : <span> </span>}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ClaimNumberToolTip = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.clm_no && (
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip title={metadata.clm_no || null} kiosk={cardSettings.kiosk}>
|
||||||
|
{metadata.clm_no ? metadata.clm_no : <span> </span>}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EmployeeAssignmentsToolTip = ({
|
||||||
|
metadata,
|
||||||
|
cardSettings,
|
||||||
|
employee_body,
|
||||||
|
employee_prep,
|
||||||
|
employee_refinish,
|
||||||
|
employee_csr
|
||||||
|
}) =>
|
||||||
|
cardSettings?.employeeassignments && (
|
||||||
|
<Col span={24}>
|
||||||
|
<Row>
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={
|
||||||
|
employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs
|
||||||
|
? `B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs ? (
|
||||||
|
`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={
|
||||||
|
employee_prep
|
||||||
|
? `P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{employee_prep ? (
|
||||||
|
`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={
|
||||||
|
employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||||
|
? `R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs ? (
|
||||||
|
`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={
|
||||||
|
employee_csr ? `C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}` : null
|
||||||
|
}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{employee_csr ? (
|
||||||
|
`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ActualInToolTip = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.actual_in && (
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip title={metadata.actual_in || null} kiosk={cardSettings.kiosk}>
|
||||||
|
{metadata.actual_in ? (
|
||||||
|
<Space>
|
||||||
|
<DownloadOutlined />
|
||||||
|
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EstimatorToolTip = ({ metadata, cardSettings }) => {
|
||||||
|
return (
|
||||||
|
cardSettings?.estimator && (
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={metadata.est_ct_fn && metadata.est_ct_ln ? `${metadata.est_ct_fn} ${metadata.est_ct_ln}` : null}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{metadata.est_ct_fn && metadata.est_ct_ln ? (
|
||||||
|
<span>E: {`${metadata.est_ct_fn} ${metadata.est_ct_ln}`}</span>
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
|
||||||
|
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
|
||||||
|
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
cardSettings?.subtotal && (
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{!!amount ? (
|
||||||
|
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScheduledCompletionToolTip = ({ metadata, cardSettings, pastDueAlert }) =>
|
||||||
|
cardSettings?.scheduled_completion && (
|
||||||
|
<Col span={cardSettings.compact ? 24 : 12}>
|
||||||
|
<EllipsesToolTip title={metadata.scheduled_completion || null} kiosk={cardSettings.kiosk}>
|
||||||
|
{metadata.scheduled_completion ? (
|
||||||
|
<Space className={pastDueAlert}>
|
||||||
|
<CalendarOutlined />
|
||||||
|
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AltTransportToolTip = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.ats && (
|
||||||
|
<Col span={12}>
|
||||||
|
<EllipsesToolTip title={metadata.alt_transport || null} kiosk={cardSettings.kiosk}>
|
||||||
|
{metadata.alt_transport ? metadata.alt_transport : <span> </span>}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SubletsComponent = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.sublets && (
|
||||||
|
<Col span={12}>
|
||||||
|
{metadata.subletLines ? (
|
||||||
|
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
|
||||||
|
) : (
|
||||||
|
<span> </span>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ProductionNoteComponent = ({ metadata, cardSettings, card }) =>
|
||||||
|
cardSettings?.production_note && (
|
||||||
|
<Col span={24} style={{ margin: "2px 0" }}>
|
||||||
|
<ProductionListColumnProductionNote
|
||||||
|
record={{
|
||||||
|
production_vars: metadata?.production_vars,
|
||||||
|
id: card?.id,
|
||||||
|
refetch: card?.refetch
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PartsStatusComponent = ({ metadata, cardSettings }) =>
|
||||||
|
cardSettings?.partsstatus && (
|
||||||
|
<Col span={24} style={{ textAlign: "center" }}>
|
||||||
|
{metadata.joblines_status ? <JobPartsQueueCount parts={metadata.joblines_status} /> : <span> </span>}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TasksToolTip = ({ metadata, cardSettings, t }) =>
|
||||||
|
cardSettings?.tasks && (
|
||||||
|
<Col span={12}>
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={`${t("production.labels.tasks")}: ${metadata.tasks_aggregate?.aggregate?.count || 0}`}
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
{metadata.tasks_aggregate?.aggregate?.count ? (
|
||||||
|
`T: ${metadata.tasks_aggregate.aggregate.count}`
|
||||||
|
) : (
|
||||||
|
<span>T: 0</span>
|
||||||
|
)}
|
||||||
|
</EllipsesToolTip>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { metadata } = card;
|
||||||
|
|
||||||
|
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
||||||
|
|
||||||
|
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
||||||
|
return {
|
||||||
|
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
|
||||||
|
employee_prep: metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep),
|
||||||
|
employee_refinish: metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish),
|
||||||
|
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
|
||||||
|
};
|
||||||
|
}, [metadata, employees]);
|
||||||
|
|
||||||
|
const pastDueAlert = useMemo(() => {
|
||||||
|
if (!metadata?.scheduled_completion) return null;
|
||||||
|
const completionDate = dayjs(metadata.scheduled_completion);
|
||||||
|
if (dayjs().isSameOrAfter(completionDate, "day")) return "production-completion-past";
|
||||||
|
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
|
||||||
|
return null;
|
||||||
|
}, [metadata?.scheduled_completion]);
|
||||||
|
|
||||||
|
const totalHrs = useMemo(() => {
|
||||||
|
return metadata?.labhrs && metadata?.larhrs
|
||||||
|
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||||
|
: 0;
|
||||||
|
}, [metadata?.labhrs, metadata?.larhrs]);
|
||||||
|
|
||||||
|
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
|
||||||
|
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
|
||||||
|
|
||||||
|
const isBodyEmpty = useMemo(() => {
|
||||||
|
return !(
|
||||||
|
cardSettings?.ownr_nm ||
|
||||||
|
cardSettings?.model_info ||
|
||||||
|
cardSettings?.ins_co_nm ||
|
||||||
|
cardSettings?.clm_no ||
|
||||||
|
cardSettings?.employeeassignments ||
|
||||||
|
cardSettings?.actual_in ||
|
||||||
|
cardSettings?.scheduled_completion ||
|
||||||
|
cardSettings?.ats ||
|
||||||
|
cardSettings?.sublets ||
|
||||||
|
cardSettings?.production_note ||
|
||||||
|
cardSettings?.partsstatus ||
|
||||||
|
cardSettings?.estimator ||
|
||||||
|
cardSettings?.subtotal ||
|
||||||
|
cardSettings?.tasks
|
||||||
|
);
|
||||||
|
}, [cardSettings]);
|
||||||
|
|
||||||
|
const headerContent = (
|
||||||
|
<div className="header-content-container">
|
||||||
|
<div className="inner-container">
|
||||||
|
<ProductionAlert id={card.id} productionVars={metadata?.production_vars} refetch={card?.refetch} key="alert" />
|
||||||
|
{metadata?.suspended && <PauseCircleOutlined className="circle-outline" key="suspended" />}
|
||||||
|
{metadata?.iouparent && (
|
||||||
|
<EllipsesToolTip
|
||||||
|
title={t("jobs.labels.iou")}
|
||||||
|
key="iouparent"
|
||||||
|
className="iouparent"
|
||||||
|
kiosk={cardSettings.kiosk}
|
||||||
|
>
|
||||||
|
<BranchesOutlined className="branches-outlined" />
|
||||||
|
</EllipsesToolTip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="tech-container">
|
||||||
|
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||||
|
{metadata?.ro_number || t("general.labels.na")}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
{isBodyEmpty && (
|
||||||
|
<div className="body-empty-container">
|
||||||
|
<Link to={{ search: `?selected=${card.id}` }}>
|
||||||
|
<EyeFilled />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const bodyContent = (
|
||||||
|
<Row>
|
||||||
|
<OwnerNameToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<ModelInfoToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<InsuranceCompanyToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<ClaimNumberToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<EmployeeAssignmentsToolTip
|
||||||
|
metadata={metadata}
|
||||||
|
cardSettings={cardSettings}
|
||||||
|
employee_body={employee_body}
|
||||||
|
employee_prep={employee_prep}
|
||||||
|
employee_refinish={employee_refinish}
|
||||||
|
employee_csr={employee_csr}
|
||||||
|
/>
|
||||||
|
<EstimatorToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<TasksToolTip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||||
|
<SubtotalTooltip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||||
|
<ActualInToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<ScheduledCompletionToolTip metadata={metadata} cardSettings={cardSettings} pastDueAlert={pastDueAlert} />
|
||||||
|
<AltTransportToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
||||||
|
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||||
|
size="small"
|
||||||
|
style={{
|
||||||
|
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||||
|
color: cardSettings?.cardcolor && contrastYIQ
|
||||||
|
}}
|
||||||
|
title={!isBodyEmpty ? headerContent : null}
|
||||||
|
extra={
|
||||||
|
!isBodyEmpty && (
|
||||||
|
<Link to={{ search: `?selected=${card.id}` }}>
|
||||||
|
<EyeFilled />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isBodyEmpty ? headerContent : bodyContent}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
|
||||||
import { Button, Card, Col, Form, notification, Popover, Row, Switch } from "antd";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
|
||||||
|
|
||||||
export default function ProductionBoardKanbanCardSettings({ associationSettings }) {
|
|
||||||
const [form] = Form.useForm();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
|
|
||||||
}, [form, associationSettings, open]);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
|
||||||
setLoading(true);
|
|
||||||
const result = await updateKbSettings({
|
|
||||||
variables: {
|
|
||||||
id: associationSettings && associationSettings.id,
|
|
||||||
ks: values
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (result.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "error",
|
|
||||||
message: t("production.errors.settings", {
|
|
||||||
error: JSON.stringify(result.errors)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setOpen(false);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const overlay = (
|
|
||||||
<div>
|
|
||||||
<Card>
|
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
|
||||||
<Row gutter={[16, 16]}>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item label={t("production.labels.compact")} name="compact" valuePropName="checked">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.ownr_nm")} name="ownr_nm">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.clm_no")} name="clm_no">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.ins_co_nm")} name="ins_co_nm">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
{/* <Form.Item
|
|
||||||
valuePropName="checked"
|
|
||||||
label={t("production.labels.laborhrs")}
|
|
||||||
name="laborhrs"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item> */}
|
|
||||||
<Form.Item
|
|
||||||
valuePropName="checked"
|
|
||||||
label={t("production.labels.employeeassignments")}
|
|
||||||
name="employeeassignments"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.actual_in")} name="actual_in">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.cardcolor")} name="cardcolor">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item
|
|
||||||
valuePropName="checked"
|
|
||||||
label={t("production.labels.scheduled_completion")}
|
|
||||||
name="scheduled_completion"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.ats")} name="ats">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.production_note")} name="production_note">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
{/* <Form.Item
|
|
||||||
valuePropName='checked' label={t("production.labels.alert")} name="alert">
|
|
||||||
<Switch/>
|
|
||||||
</Form.Item> */}
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.sublets")} name="sublets">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.partsstatus")} name="partsstatus">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item valuePropName="checked" label={t("production.labels.stickyheader")} name="stickyheader">
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
form.submit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.save")}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Popover content={overlay} open={open} placement="topRight">
|
|
||||||
<Button loading={loading} onClick={() => setOpen(true)}>
|
|
||||||
{t("production.labels.cardsettings")}
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,129 +1,164 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import Board, { moveCard } from "@asseinfo/react-kanban";
|
import Board from "./trello-board/index";
|
||||||
import { Button, Grid, notification, Space, Statistic } from "antd";
|
import { Button, notification, Skeleton, Space } from "antd";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Sticky, StickyContainer } from "react-sticky";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import styled from "styled-components";
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
|
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
|
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
|
||||||
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
|
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
|
||||||
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
|
|
||||||
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
|
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
|
||||||
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
|
import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx";
|
||||||
//import "@asseinfo/react-kanban/dist/styles.css";
|
|
||||||
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
|
|
||||||
import "./production-board-kanban.styles.scss";
|
import "./production-board-kanban.styles.scss";
|
||||||
import { createBoardData } from "./production-board-kanban.utils.js";
|
import { createBoardData } from "./production-board-kanban.utils.js";
|
||||||
|
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
import { mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
||||||
|
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop
|
||||||
technician: selectTechnician
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
});
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
export function ProductionBoardKanbanComponent({
|
jobid,
|
||||||
data,
|
operation,
|
||||||
bodyshop,
|
type
|
||||||
refetch,
|
})
|
||||||
technician,
|
)
|
||||||
insertAuditTrail,
|
|
||||||
associationSettings
|
|
||||||
}) {
|
|
||||||
const [boardLanes, setBoardLanes] = useState({
|
|
||||||
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
|
||||||
|
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
|
||||||
const [filter, setFilter] = useState({ search: "", employeeId: null });
|
const [filter, setFilter] = useState({ search: "", employeeId: null });
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [isMoving, setIsMoving] = useState(false);
|
const [isMoving, setIsMoving] = useState(false);
|
||||||
|
const [orientation, setOrientation] = useState("vertical");
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
|
||||||
const boardData = createBoardData(
|
|
||||||
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
|
|
||||||
data,
|
|
||||||
filter
|
|
||||||
);
|
|
||||||
|
|
||||||
boardData.columns = boardData.columns.map((d) => {
|
|
||||||
return { ...d, title: `${d.title} (${d.cards.length})` };
|
|
||||||
});
|
|
||||||
setBoardLanes(boardData);
|
|
||||||
setIsMoving(false);
|
|
||||||
}, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
|
|
||||||
|
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
const handleDragEnd = async (card, source, destination) => {
|
useEffect(() => {
|
||||||
|
if (associationSettings) {
|
||||||
|
setLoading(true);
|
||||||
|
setOrientation(associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [associationSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMoving(true);
|
||||||
|
const newBoardData = createBoardData({
|
||||||
|
statuses,
|
||||||
|
data,
|
||||||
|
filter,
|
||||||
|
cardSettings: associationSettings?.kanban_settings
|
||||||
|
});
|
||||||
|
|
||||||
|
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
|
||||||
|
...lane,
|
||||||
|
title: `${lane.title} (${lane.cards.length})`
|
||||||
|
}));
|
||||||
|
|
||||||
|
setBoardLanes((prevBoardLanes) => {
|
||||||
|
const deepClonedData = cloneDeep(newBoardData);
|
||||||
|
if (!isEqual(prevBoardLanes, deepClonedData)) {
|
||||||
|
return deepClonedData;
|
||||||
|
}
|
||||||
|
return prevBoardLanes;
|
||||||
|
});
|
||||||
|
setIsMoving(false);
|
||||||
|
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
|
||||||
|
|
||||||
|
const getCardByID = useCallback((data, cardId) => {
|
||||||
|
for (const lane of data.lanes) {
|
||||||
|
for (const card of lane.cards) {
|
||||||
|
if (card.id === cardId) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onDragEnd = useCallback(
|
||||||
|
async ({ type, source, destination, draggableId }) => {
|
||||||
logImEXEvent("kanban_drag_end");
|
logImEXEvent("kanban_drag_end");
|
||||||
|
|
||||||
|
if (!type || type !== "lane" || !source || !destination || isMoving) return;
|
||||||
|
|
||||||
setIsMoving(true);
|
setIsMoving(true);
|
||||||
setBoardLanes(moveCard(boardLanes, source, destination));
|
|
||||||
const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
|
|
||||||
const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
|
|
||||||
const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
|
|
||||||
|
|
||||||
const movedCardWillBeFirst = destination.toPosition === 0;
|
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
|
||||||
|
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
|
||||||
|
|
||||||
const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
|
if (!targetLane || !sourceLane) {
|
||||||
|
setIsMoving(false);
|
||||||
|
console.error("Invalid source or destination lane");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
|
const sameColumnTransfer = source.droppableId === destination.droppableId;
|
||||||
|
const sourceCard = getCardByID(boardLanes, draggableId);
|
||||||
|
|
||||||
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
|
const movedCardWillBeFirst = destination.index === 0;
|
||||||
|
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
|
||||||
|
|
||||||
|
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
|
||||||
|
const oldChildCard = sourceLane.cards[source.index + 1];
|
||||||
|
|
||||||
const newChildCard = movedCardWillBeLast
|
const newChildCard = movedCardWillBeLast
|
||||||
? null
|
? null
|
||||||
: destinationColumn.cards[
|
: targetLane.cards[
|
||||||
sameColumnTransfer
|
sameColumnTransfer
|
||||||
? source.fromPosition - destination.toPosition > 0
|
? source.index < destination.index
|
||||||
? destination.toPosition
|
? destination.index + 1
|
||||||
: destination.toPosition + 1
|
: destination.index
|
||||||
: destination.toPosition
|
: destination.index
|
||||||
];
|
];
|
||||||
|
|
||||||
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
|
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
|
||||||
|
|
||||||
let movedCardNewKanbanParent;
|
let movedCardNewKanbanParent;
|
||||||
if (movedCardWillBeFirst) {
|
if (movedCardWillBeFirst) {
|
||||||
//console.log("==> New Card is first.");
|
|
||||||
movedCardNewKanbanParent = "-1";
|
movedCardNewKanbanParent = "-1";
|
||||||
} else if (movedCardWillBeLast) {
|
} else if (movedCardWillBeLast) {
|
||||||
// console.log("==> New Card is last.");
|
movedCardNewKanbanParent = lastCardInTargetLane.id;
|
||||||
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
|
} else if (newChildCard) {
|
||||||
} else if (!!newChildCard) {
|
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
|
||||||
// console.log("==> New Card is somewhere in the middle");
|
|
||||||
movedCardNewKanbanParent = newChildCard.kanbanparent;
|
|
||||||
} else {
|
} else {
|
||||||
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
|
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||||
}
|
}
|
||||||
const newChildCardNewParent = newChildCard ? card.id : null;
|
|
||||||
|
const newChildCardNewParent = newChildCard ? draggableId : null;
|
||||||
|
|
||||||
|
try {
|
||||||
const update = await client.mutate({
|
const update = await client.mutate({
|
||||||
mutation: generate_UPDATE_JOB_KANBAN(
|
mutation: generate_UPDATE_JOB_KANBAN(
|
||||||
oldChildCard ? oldChildCard.id : null,
|
oldChildCard ? oldChildCard.id : null,
|
||||||
oldChildCardNewParent,
|
oldChildCardNewParent,
|
||||||
card.id,
|
draggableId,
|
||||||
movedCardNewKanbanParent,
|
movedCardNewKanbanParent,
|
||||||
destination.toColumnId,
|
targetLane.id,
|
||||||
newChildCard ? newChildCard.id : null,
|
newChildCard ? newChildCard.id : null,
|
||||||
newChildCardNewParent
|
newChildCardNewParent
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: card.id,
|
jobid: draggableId,
|
||||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
|
||||||
type: "jobstatuschange"
|
type: "jobstatuschange"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,132 +169,69 @@ export function ProductionBoardKanbanComponent({
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("production.errors.boardupdate", {
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsMoving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
|
||||||
|
);
|
||||||
|
|
||||||
const totalHrs = data
|
const cardSettings = useMemo(() => {
|
||||||
.reduce(
|
const kanbanSettings = associationSettings?.kanban_settings;
|
||||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
return mergeWithDefaults(kanbanSettings);
|
||||||
0
|
}, [associationSettings]);
|
||||||
)
|
|
||||||
.toFixed(1);
|
|
||||||
const totalLAB = data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
|
|
||||||
const totalLAR = data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
|
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
|
||||||
.filter((screen) => !!screen[1])
|
|
||||||
.slice(-1)[0];
|
|
||||||
|
|
||||||
const standardSizes = {
|
const handleSettingsChange = useCallback((newSettings) => {
|
||||||
xs: "250",
|
setLoading(true);
|
||||||
sm: "250",
|
setOrientation(newSettings.orientation ? "vertical" : "horizontal");
|
||||||
md: "250",
|
setLoading(false);
|
||||||
lg: "250",
|
}, []);
|
||||||
xl: "250",
|
|
||||||
xxl: "250"
|
|
||||||
};
|
|
||||||
const compactSizes = {
|
|
||||||
xs: "150",
|
|
||||||
sm: "150",
|
|
||||||
md: "150",
|
|
||||||
lg: "150",
|
|
||||||
xl: "155",
|
|
||||||
xxl: "155"
|
|
||||||
};
|
|
||||||
|
|
||||||
const width = selectedBreakpoint
|
if (loading) {
|
||||||
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
|
return <Skeleton active />;
|
||||||
? compactSizes[selectedBreakpoint[0]]
|
}
|
||||||
: standardSizes[selectedBreakpoint[0]]
|
|
||||||
: "250";
|
|
||||||
|
|
||||||
const stickyHeader = {
|
|
||||||
renderColumnHeader: ({ title }) => (
|
|
||||||
<Sticky>
|
|
||||||
{({
|
|
||||||
style,
|
|
||||||
|
|
||||||
// the following are also available but unused in this example
|
|
||||||
isSticky,
|
|
||||||
wasSticky,
|
|
||||||
distanceFromTop,
|
|
||||||
distanceFromBottom,
|
|
||||||
calculatedHeight
|
|
||||||
}) => (
|
|
||||||
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Sticky>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardSettings =
|
|
||||||
associationSettings &&
|
|
||||||
associationSettings.kanban_settings &&
|
|
||||||
Object.keys(associationSettings.kanban_settings).length > 0
|
|
||||||
? associationSettings.kanban_settings
|
|
||||||
: {
|
|
||||||
ats: true,
|
|
||||||
clm_no: true,
|
|
||||||
compact: false,
|
|
||||||
ownr_nm: true,
|
|
||||||
sublets: true,
|
|
||||||
ins_co_nm: true,
|
|
||||||
production_note: true,
|
|
||||||
employeeassignments: true,
|
|
||||||
scheduled_completion: true,
|
|
||||||
stickyheader: false,
|
|
||||||
cardcolor: false
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container width={width}>
|
<div>
|
||||||
<IndefiniteLoading loading={isMoving} />
|
<IndefiniteLoading loading={isMoving} />
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title={cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||||
<Space>
|
style={{ paddingInline: 0, paddingBlock: 0 }}
|
||||||
<Statistic title={t("dashboard.titles.productionhours")} value={totalHrs} />
|
|
||||||
<Statistic title={t("dashboard.titles.labhours")} value={totalLAB} />
|
|
||||||
<Statistic title={t("dashboard.titles.larhours")} value={totalLAR} />
|
|
||||||
<Statistic title={t("appointments.labels.inproduction")} value={data && data.length} />
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button onClick={() => refetch && refetch()}>
|
<Button onClick={() => refetch && refetch()}>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
||||||
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
|
<ProductionBoardKanbanSettings
|
||||||
|
parentLoading={setLoading}
|
||||||
|
associationSettings={associationSettings}
|
||||||
|
onSettingsChange={handleSettingsChange}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
<NoteUpsertModal />
|
||||||
|
|
||||||
<ProductionListDetailComponent jobs={data} />
|
<ProductionListDetailComponent jobs={data} />
|
||||||
<StickyContainer>
|
|
||||||
<Board
|
<Board
|
||||||
style={{ height: "100%" }}
|
queryData={data}
|
||||||
children={boardLanes}
|
data={boardLanes}
|
||||||
disableCardDrag={isMoving}
|
onDragEnd={onDragEnd}
|
||||||
{...(cardSettings.stickyheader && stickyHeader)}
|
orientation={orientation}
|
||||||
renderCard={(card) => ProductionBoardCard(technician, card, bodyshop, cardSettings)}
|
cardSettings={cardSettings}
|
||||||
onCardDragEnd={handleDragEnd}
|
|
||||||
/>
|
/>
|
||||||
</StickyContainer>
|
</div>
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
.react-kanban-card-skeleton,
|
|
||||||
.react-kanban-card,
|
|
||||||
.react-kanban-card-adder-form {
|
|
||||||
box-sizing: border-box;
|
|
||||||
max-width: ${(props) => props.width}px;
|
|
||||||
min-width: ${(props) => props.width}px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import _ from "lodash";
|
import { useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
|
||||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
|
||||||
QUERY_JOBS_IN_PRODUCTION,
|
|
||||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
|
||||||
} from "../../graphql/jobs.queries";
|
|
||||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||||
@@ -18,76 +12,53 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||||
|
const combinedStatuses = useMemo(
|
||||||
|
() => [
|
||||||
|
...bodyshop.md_ro_statuses.production_statuses,
|
||||||
|
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
|
||||||
|
],
|
||||||
|
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
|
||||||
|
);
|
||||||
|
|
||||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||||
pollInterval: 3600000,
|
pollInterval: 3600000,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only",
|
||||||
|
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||||
});
|
});
|
||||||
const client = useApolloClient();
|
|
||||||
const [joblist, setJoblist] = useState([]);
|
|
||||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||||
if (!(data && data.jobs)) return;
|
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||||
setJoblist(
|
|
||||||
data.jobs.map((j) => {
|
|
||||||
return { id: j.id, updated_at: j.updated_at };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!updatedJobs || joblist.length === 0) return;
|
|
||||||
|
|
||||||
const jobDiff = _.differenceWith(
|
|
||||||
joblist,
|
|
||||||
updatedJobs.jobs,
|
|
||||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
|
||||||
);
|
|
||||||
|
|
||||||
jobDiff.forEach((job) => {
|
|
||||||
getUpdatedJobData(job.id);
|
|
||||||
});
|
});
|
||||||
if (jobDiff.length > 1) {
|
|
||||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
|
||||||
} else if (jobDiff.length === 1) {
|
|
||||||
jobDiff.forEach((job) => {
|
|
||||||
getUpdatedJobData(job.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setJoblist(updatedJobs.jobs);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [updatedJobs]);
|
|
||||||
|
|
||||||
const getUpdatedJobData = async (jobId) => {
|
|
||||||
client.query({
|
|
||||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
|
||||||
variables: { id: jobId }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const getUpdatedJobsData = async (jobIds) => {
|
|
||||||
client.query({
|
|
||||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
|
||||||
variables: { ids: jobIds }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||||
variables: { email: currentUser.email }
|
variables: { email: currentUser.email },
|
||||||
|
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updatedJobs && data) {
|
||||||
|
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||||
|
}
|
||||||
|
}, [updatedJobs, data, refetch]);
|
||||||
|
|
||||||
|
const filteredAssociationSettings = useMemo(() => {
|
||||||
|
return associationSettings?.associations[0] || null;
|
||||||
|
}, [associationSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductionBoardKanbanComponent
|
<ProductionBoardKanbanComponent
|
||||||
loading={loading || associationSettingsLoading}
|
loading={loading || associationSettingsLoading}
|
||||||
data={data ? data.jobs : []}
|
data={data ? data.jobs : []}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
associationSettings={
|
associationSettings={filteredAssociationSettings}
|
||||||
associationSettings && associationSettings.associations[0] ? associationSettings.associations[0] : null
|
bodyshop={bodyshop}
|
||||||
}
|
statuses={combinedStatuses}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(ProductionBoardKanbanContainer);
|
export default connect(mapStateToProps)(ProductionBoardKanbanContainer);
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Card, Statistic } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
|
||||||
|
|
||||||
|
export const StatisticType = {
|
||||||
|
HOURS: "hours",
|
||||||
|
AMOUNT: "amount",
|
||||||
|
JOBS: "jobs",
|
||||||
|
TASKS: "tasks"
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeStatistics = (items, values) => {
|
||||||
|
const valuesMap = values.reduce((acc, value) => {
|
||||||
|
acc[value.id] = value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
value: valuesMap[item.id]?.value,
|
||||||
|
type: valuesMap[item.id]?.type
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const calculateTotal = (items, key, subKey) => {
|
||||||
|
return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateTotalAmount = (items, key) => {
|
||||||
|
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateReducerTotal = (lanes, key, subKey) => {
|
||||||
|
return lanes.reduce((acc, lane) => {
|
||||||
|
return (
|
||||||
|
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.aggregate?.sum?.[subKey] || 0), 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateReducerTotalAmount = (lanes, key) => {
|
||||||
|
return lanes.reduce((acc, lane) => {
|
||||||
|
return (
|
||||||
|
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValue = (value, type) => {
|
||||||
|
if (type === StatisticType.JOBS) {
|
||||||
|
return value.toFixed(0);
|
||||||
|
}
|
||||||
|
if (type === StatisticType.HOURS) {
|
||||||
|
return value.toFixed(2);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalHrs = useMemo(() => {
|
||||||
|
if (!cardSettings.totalHrs) return null;
|
||||||
|
const total = calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [data, cardSettings.totalHrs]);
|
||||||
|
|
||||||
|
const totalLAB = useMemo(() => {
|
||||||
|
if (!cardSettings.totalLAB) return null;
|
||||||
|
const total = calculateTotal(data, "labhrs", "mod_lb_hrs");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [data, cardSettings.totalLAB]);
|
||||||
|
|
||||||
|
const totalLAR = useMemo(() => {
|
||||||
|
if (!cardSettings.totalLAR) return null;
|
||||||
|
const total = calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [data, cardSettings.totalLAR]);
|
||||||
|
|
||||||
|
const jobsInProduction = useMemo(
|
||||||
|
() => (cardSettings.jobsInProduction ? data.length : null),
|
||||||
|
[data, cardSettings.jobsInProduction]
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalAmountInProduction = useMemo(() => {
|
||||||
|
if (!cardSettings.totalAmountInProduction) return null;
|
||||||
|
const total = calculateTotalAmount(data, "job_totals");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [data, cardSettings.totalAmountInProduction]);
|
||||||
|
|
||||||
|
const totalHrsOnBoard = useMemo(() => {
|
||||||
|
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
|
||||||
|
const total =
|
||||||
|
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
|
||||||
|
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [reducerData, cardSettings.totalHrsOnBoard]);
|
||||||
|
|
||||||
|
const totalLABOnBoard = useMemo(() => {
|
||||||
|
if (!reducerData || !cardSettings.totalLABOnBoard) return null;
|
||||||
|
const total = calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [reducerData, cardSettings.totalLABOnBoard]);
|
||||||
|
|
||||||
|
const totalLAROnBoard = useMemo(() => {
|
||||||
|
if (!reducerData || !cardSettings.totalLAROnBoard) return null;
|
||||||
|
const total = calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [reducerData, cardSettings.totalLAROnBoard]);
|
||||||
|
|
||||||
|
const jobsOnBoard = useMemo(
|
||||||
|
() =>
|
||||||
|
reducerData && cardSettings.jobsOnBoard
|
||||||
|
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
|
||||||
|
: null,
|
||||||
|
[reducerData, cardSettings.jobsOnBoard]
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalAmountOnBoard = useMemo(() => {
|
||||||
|
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
|
||||||
|
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
|
||||||
|
return parseFloat(total.toFixed(2));
|
||||||
|
}, [reducerData, cardSettings.totalAmountOnBoard]);
|
||||||
|
|
||||||
|
const tasksInProduction = useMemo(() => {
|
||||||
|
if (!data || !cardSettings.tasksInProduction) return null;
|
||||||
|
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
|
||||||
|
}, [data, cardSettings.tasksInProduction]);
|
||||||
|
|
||||||
|
const tasksOnBoard = useMemo(() => {
|
||||||
|
if (!reducerData || !cardSettings.tasksOnBoard) return null;
|
||||||
|
return reducerData.lanes.reduce((acc, lane) => {
|
||||||
|
return (
|
||||||
|
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
}, [reducerData, cardSettings.tasksOnBoard]);
|
||||||
|
|
||||||
|
const statistics = useMemo(
|
||||||
|
() =>
|
||||||
|
mergeStatistics(statisticsItems, [
|
||||||
|
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
|
||||||
|
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
|
||||||
|
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
|
||||||
|
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
|
||||||
|
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
|
||||||
|
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
|
||||||
|
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
|
||||||
|
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
|
||||||
|
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
|
||||||
|
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
|
||||||
|
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
|
||||||
|
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
totalHrs,
|
||||||
|
totalAmountInProduction,
|
||||||
|
totalLAB,
|
||||||
|
totalLAR,
|
||||||
|
jobsInProduction,
|
||||||
|
totalHrsOnBoard,
|
||||||
|
totalAmountOnBoard,
|
||||||
|
totalLABOnBoard,
|
||||||
|
totalLAROnBoard,
|
||||||
|
jobsOnBoard,
|
||||||
|
tasksOnBoard,
|
||||||
|
tasksInProduction
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedStatistics = useMemo(() => {
|
||||||
|
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
|
||||||
|
).reduce((sorted, orderId) => {
|
||||||
|
const value = statisticsMap.get(orderId);
|
||||||
|
if (value && value.value !== null) {
|
||||||
|
sorted.push(value);
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
|
}, []);
|
||||||
|
}, [statistics, cardSettings.statisticsOrder]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", gap: "5px", flexWrap: "wrap", marginBottom: "5px" }}>
|
||||||
|
{sortedStatistics.map((stat) => (
|
||||||
|
<Card styles={{ body: { padding: "8px" } }} key={stat.id}>
|
||||||
|
<Statistic
|
||||||
|
title={t(`production.statistics.${stat.label}`)}
|
||||||
|
value={formatValue(stat.value, stat.type)}
|
||||||
|
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
|
||||||
|
suffix={
|
||||||
|
stat.type === StatisticType.HOURS
|
||||||
|
? t("production.statistics.hours")
|
||||||
|
: stat.type === StatisticType.JOBS
|
||||||
|
? t("production.statistics.jobs")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProductionStatistics.propTypes = {
|
||||||
|
data: PropTypes.array.isRequired,
|
||||||
|
cardSettings: PropTypes.object.isRequired,
|
||||||
|
reducerData: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionStatistics;
|
||||||
@@ -1,145 +1,72 @@
|
|||||||
.react-kanban-board {
|
.react-trello-board {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-card {
|
.height-preserving-container:empty {
|
||||||
border-radius: 3px;
|
min-height: calc(var(--child-height));
|
||||||
background-color: #fff;
|
box-sizing: border-box;
|
||||||
padding: 4px;
|
|
||||||
margin-bottom: 7px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// .react-kanban-card-skeleton,
|
.height-preserving-container {
|
||||||
// .react-kanban-card,
|
|
||||||
// .react-kanban-card-adder-form {
|
|
||||||
// box-sizing: border-box;
|
|
||||||
// max-width: 145px;
|
|
||||||
// min-width: 145px;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.react-kanban-card--dragging {
|
|
||||||
box-shadow: 2px 2px grey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-card__description {
|
.react-trello-column-header {
|
||||||
padding-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card__title {
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-column {
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: #eee;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-column input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form {
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: #fff;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 7px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form input {
|
|
||||||
border: 0px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-button {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 5px;
|
|
||||||
background-color: transparent;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #ccc;
|
background-color: #d0d0d0;
|
||||||
transition: 0.3s;
|
border-radius: 5px 5px 0 0;
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-card-adder-button:hover {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form__title {
|
.production-alert {
|
||||||
font-weight: bold;
|
background: transparent;
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form__title:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form__description {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form__description:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-kanban-card-adder-form__button {
|
|
||||||
background-color: #eee;
|
|
||||||
border: none;
|
border: none;
|
||||||
padding: 5px;
|
}
|
||||||
width: 45%;
|
.react-trello-footer {
|
||||||
margin-top: 5px;
|
background-color: #d0d0d0;
|
||||||
border-radius: 3px;
|
border-radius: 0 0 5px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-card-adder-form__button:hover {
|
.grid-item {
|
||||||
transition: 0.3s;
|
margin: 1px; // TODO: (Note) THis is where we set the margin for vertical
|
||||||
cursor: pointer;
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-column-header {
|
.lane-title {
|
||||||
padding-bottom: 10px;
|
vertical-align: middle;
|
||||||
font-weight: bold;
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 8px; /* Adjust the spacing as needed */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-column-header input:focus {
|
.header-content-container {
|
||||||
outline: none;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
.body-empty-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
.tech-container {
|
||||||
.react-kanban-column-header__button {
|
font-weight: bolder;
|
||||||
color: #333333;
|
text-align: center;
|
||||||
background-color: #ffffff;
|
flex: 1;
|
||||||
border-color: #cccccc;
|
.branches-outlined {
|
||||||
|
color: orangered;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-column-header__button:hover,
|
|
||||||
.react-kanban-column-header__button:focus,
|
|
||||||
.react-kanban-column-header__button:active {
|
|
||||||
background-color: #e6e6e6;
|
|
||||||
}
|
}
|
||||||
|
.inner-container {
|
||||||
.react-kanban-column-adder-button {
|
display: flex;
|
||||||
border: 2px dashed #eee;
|
align-items: center;
|
||||||
height: 132px;
|
position: absolute;
|
||||||
margin: 5px;
|
left: 0;
|
||||||
|
.circle-outline {
|
||||||
|
color: orangered;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.iou-parent {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-kanban-column-adder-button:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,121 +1,102 @@
|
|||||||
|
// Function to sort an array of objects by parentId
|
||||||
import { groupBy } from "lodash";
|
import { groupBy } from "lodash";
|
||||||
|
|
||||||
const sortByParentId = (arr) => {
|
const sortByParentId = (arr) => {
|
||||||
// return arr.reduce((accumulator, currentValue) => {
|
|
||||||
// //Find the parent item.
|
|
||||||
// let item = accumulator.find((x) => x.id === currentValue.kanbanparent);
|
|
||||||
// //Get index of parent item
|
|
||||||
// let index = accumulator.indexOf(item);
|
|
||||||
|
|
||||||
// index = index !== -1 ? index + 1 : 0;
|
|
||||||
// accumulator.splice(index, 0, currentValue);
|
|
||||||
// return accumulator;
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
let parentId = "-1";
|
let parentId = "-1";
|
||||||
const sortedList = [];
|
const sortedList = [];
|
||||||
const byParentsIdsList = groupBy(arr, "kanbanparent"); // Create a new array with objects indexed by parentId
|
const byParentsIdsList = groupBy(arr, "kanbanparent");
|
||||||
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
|
|
||||||
|
|
||||||
while (byParentsIdsList[parentId]) {
|
while (byParentsIdsList[parentId]) {
|
||||||
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
|
sortedList.push(...byParentsIdsList[parentId]);
|
||||||
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
|
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
|
if (byParentsIdsList["null"]) {
|
||||||
|
sortedList.push(...byParentsIdsList["null"]);
|
||||||
|
}
|
||||||
|
|
||||||
//Validate that the 2 arrays are of the same length and no children are missing.
|
// Ensure all items are included in the sorted list
|
||||||
if (arr.length !== sortedList.length) {
|
if (arr.length !== sortedList.length) {
|
||||||
arr.map((origItem) => {
|
arr.forEach((origItem) => {
|
||||||
if (!!!sortedList.find((s) => s.id === origItem.id)) {
|
if (!sortedList.some((s) => s.id === origItem.id)) {
|
||||||
sortedList.push(origItem);
|
sortedList.push(origItem);
|
||||||
console.log("DATA CONSISTENCY ERROR: ", origItem.ro_number);
|
|
||||||
}
|
}
|
||||||
return 1;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedList;
|
return sortedList;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createBoardData = (AllStatuses, Jobs, filter) => {
|
// Function to create board data based on statuses and jobs, with optional filtering
|
||||||
|
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
||||||
const { search, employeeId } = filter;
|
const { search, employeeId } = filter;
|
||||||
const boardLanes = {
|
|
||||||
columns: AllStatuses.map((s) => {
|
const lanes = statuses.map((status) => ({
|
||||||
return {
|
id: status,
|
||||||
id: s,
|
title: status,
|
||||||
title: s,
|
|
||||||
cards: []
|
cards: []
|
||||||
};
|
}));
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredJobs =
|
let filteredJobs =
|
||||||
(search === "" || !search) && !employeeId
|
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||||
? Jobs
|
|
||||||
: Jobs.filter((j) => {
|
// Filter jobs by selectedMdInsCos if it has values
|
||||||
let include = false;
|
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
||||||
if (search && search !== "") {
|
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
||||||
include = CheckSearch(search, j);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!employeeId) {
|
// Filter jobs by selectedEstimators if it has values
|
||||||
include =
|
if (cardSettings?.selectedEstimators?.length > 0) {
|
||||||
include ||
|
filteredJobs = filteredJobs.filter((job) =>
|
||||||
j.employee_body === employeeId ||
|
cardSettings.selectedEstimators.includes(`${job.est_ct_fn} ${job.est_ct_ln}`)
|
||||||
j.employee_prep === employeeId ||
|
|
||||||
j.employee_csr === employeeId ||
|
|
||||||
j.employee_refinish === employeeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return include;
|
|
||||||
});
|
|
||||||
|
|
||||||
const DataGroupedByStatus = groupBy(filteredJobs, (d) => d.status);
|
|
||||||
|
|
||||||
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
|
|
||||||
try {
|
|
||||||
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
|
|
||||||
if (!needle?.cards) return null;
|
|
||||||
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error while creating board card", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return boardLanes;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CheckSearch = (search, job) => {
|
|
||||||
return (
|
|
||||||
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DataGroupedByStatus = groupBy(filteredJobs, "status");
|
||||||
|
|
||||||
|
Object.keys(DataGroupedByStatus).forEach((statusGroupKey) => {
|
||||||
|
try {
|
||||||
|
const lane = lanes.find((l) => l.id === statusGroupKey);
|
||||||
|
if (!lane) return;
|
||||||
|
|
||||||
|
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
|
||||||
|
const { id, title, description, due_date, ...metadata } = job;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
label: due_date || "",
|
||||||
|
metadata
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error while creating board card", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { lanes };
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const updateBoardOnMove = (board, card, source, destination) => {
|
// Function to check if a job matches the search and/or employeeId filter
|
||||||
// //Slice from source
|
const checkFilter = (search, employeeId, job) => {
|
||||||
|
const lowerSearch = search?.toLowerCase() ?? "";
|
||||||
|
|
||||||
// const sourceCardList = board.columns.find((x) => x.id === source.fromColumnId)
|
const matchesSearch =
|
||||||
// .cards;
|
lowerSearch &&
|
||||||
// sourceCardList.slice(source.fromPosition, 0);
|
[
|
||||||
|
job.ro_number,
|
||||||
|
job.ownr_fn,
|
||||||
|
job.ownr_co_nm,
|
||||||
|
job.ownr_ln,
|
||||||
|
job.status,
|
||||||
|
job.v_make_desc,
|
||||||
|
job.v_model_desc,
|
||||||
|
job.clm_no,
|
||||||
|
job.plate_no
|
||||||
|
].some((field) => field?.toLowerCase().includes(lowerSearch));
|
||||||
|
|
||||||
// //Splice into destination.
|
const matchesEmployeeId =
|
||||||
// const destCardList = board.columns.find(
|
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
|
||||||
// (x) => x.id === destination.toColumnId
|
|
||||||
// ).cards;
|
|
||||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
|
||||||
|
|
||||||
// destCardList.splice(destination.toPosition, 0, card);
|
return matchesSearch || matchesEmployeeId;
|
||||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
};
|
||||||
// console.log("board", board);
|
|
||||||
// return board;
|
|
||||||
// };
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, Form, Select } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const FilterSettings = ({
|
||||||
|
selectedMdInsCos,
|
||||||
|
setSelectedMdInsCos,
|
||||||
|
selectedEstimators,
|
||||||
|
setSelectedEstimators,
|
||||||
|
setHasChanges,
|
||||||
|
bodyshop,
|
||||||
|
data
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const extractNames = (source, firstNameKey, lastNameKey) =>
|
||||||
|
source.map((item) => ({
|
||||||
|
firstName: item[firstNameKey],
|
||||||
|
lastName: item[lastNameKey]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bodyshopNames = extractNames(bodyshop.md_estimators, "est_ct_fn", "est_ct_ln");
|
||||||
|
const dataNames = extractNames(data, "est_ct_fn", "est_ct_ln");
|
||||||
|
|
||||||
|
const combinedNames = [...bodyshopNames, ...dataNames];
|
||||||
|
|
||||||
|
const uniqueNames = Array.from(
|
||||||
|
new Map(combinedNames.map((item) => [`${item.firstName} ${item.lastName}`, item])).values()
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("production.settings.filters_title")}>
|
||||||
|
<Form.Item label={t("production.settings.filters.md_ins_cos")}>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder={t("production.settings.filters.md_ins_cos")}
|
||||||
|
value={selectedMdInsCos}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedMdInsCos(value);
|
||||||
|
setHasChanges(true);
|
||||||
|
}}
|
||||||
|
options={bodyshop.md_ins_cos.map((item) => ({
|
||||||
|
value: item.name,
|
||||||
|
label: item.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("production.settings.filters.md_estimators")}>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder={t("production.settings.filters.md_estimators")}
|
||||||
|
value={selectedEstimators}
|
||||||
|
onChange={(value) => {
|
||||||
|
setSelectedEstimators(value);
|
||||||
|
setHasChanges(true);
|
||||||
|
}}
|
||||||
|
options={uniqueNames.map((item) => {
|
||||||
|
const name = `${item.firstName} ${item.lastName}`.trim();
|
||||||
|
return {
|
||||||
|
value: name,
|
||||||
|
label: name
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FilterSettings.propTypes = {
|
||||||
|
selectedMdInsCos: PropTypes.array.isRequired,
|
||||||
|
setSelectedMdInsCos: PropTypes.func.isRequired,
|
||||||
|
setHasChanges: PropTypes.func.isRequired,
|
||||||
|
selectedEstimators: PropTypes.array.isRequired,
|
||||||
|
setSelectedEstimators: PropTypes.func,
|
||||||
|
bodyshop: PropTypes.object.isRequired,
|
||||||
|
data: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FilterSettings;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const InformationSettings = ({ t }) => (
|
||||||
|
<Card title={t("production.settings.information")}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{[
|
||||||
|
"model_info",
|
||||||
|
"ownr_nm",
|
||||||
|
"clm_no",
|
||||||
|
"ins_co_nm",
|
||||||
|
"employeeassignments",
|
||||||
|
"actual_in",
|
||||||
|
"scheduled_completion",
|
||||||
|
"ats",
|
||||||
|
"production_note",
|
||||||
|
"sublets",
|
||||||
|
"partsstatus",
|
||||||
|
"estimator",
|
||||||
|
"subtotal",
|
||||||
|
"tasks"
|
||||||
|
].map((item) => (
|
||||||
|
<Col span={4} key={item}>
|
||||||
|
<Form.Item name={item} valuePropName="checked">
|
||||||
|
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
InformationSettings.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InformationSettings;
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Card, Col, Form, Radio, Row } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const LayoutSettings = ({ t }) => (
|
||||||
|
<Card title={t("production.settings.layout")}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
name: "orientation",
|
||||||
|
label: t("production.labels.orientation"),
|
||||||
|
options: [
|
||||||
|
{ value: true, label: t("production.labels.vertical") },
|
||||||
|
{ value: false, label: t("production.labels.horizontal") }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cardSize",
|
||||||
|
label: t("production.labels.card_size"),
|
||||||
|
options: [
|
||||||
|
{ value: "small", label: t("production.options.small") },
|
||||||
|
{ value: "medium", label: t("production.options.medium") },
|
||||||
|
{ value: "large", label: t("production.options.large") }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "compact",
|
||||||
|
label: t("production.labels.compact"),
|
||||||
|
options: [
|
||||||
|
{ value: true, label: t("production.labels.tall") },
|
||||||
|
{ value: false, label: t("production.labels.wide") }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cardcolor",
|
||||||
|
label: t("production.labels.cardcolor"),
|
||||||
|
options: [
|
||||||
|
{ value: true, label: t("production.labels.on") },
|
||||||
|
{ value: false, label: t("production.labels.off") }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kiosk",
|
||||||
|
label: t("production.labels.kiosk_mode"),
|
||||||
|
options: [
|
||||||
|
{ value: true, label: t("production.labels.on") },
|
||||||
|
{ value: false, label: t("production.labels.off") }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
].map(({ name, label, options }) => (
|
||||||
|
<Col span={4} key={name}>
|
||||||
|
<Form.Item name={name} label={label}>
|
||||||
|
<Radio.Group>
|
||||||
|
{options.map((option) => (
|
||||||
|
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
LayoutSettings.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutSettings;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||||
|
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||||
|
import { Card, Checkbox, Form } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||||
|
const onDragEnd = (result) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
const newOrder = Array.from(statisticsOrder);
|
||||||
|
const [movedItem] = newOrder.splice(result.source.index, 1);
|
||||||
|
newOrder.splice(result.destination.index, 0, movedItem);
|
||||||
|
setStatisticsOrder(newOrder);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("production.settings.statistics_title")}>
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable direction="grid" droppableId="statistics">
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}
|
||||||
|
>
|
||||||
|
{statisticsOrder.map((itemId, index) => {
|
||||||
|
const item = statisticsItems.find((stat) => stat.id === itemId);
|
||||||
|
return (
|
||||||
|
<Draggable key={itemId} draggableId={itemId.toString()} index={index}>
|
||||||
|
{(provided) => (
|
||||||
|
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||||
|
<Card styles={{ body: { padding: "5px" } }} style={{ marginBottom: 8, flex: "0 1 auto" }}>
|
||||||
|
<Form.Item style={{ marginBottom: 0 }} name={item.name} valuePropName="checked">
|
||||||
|
<Checkbox>{t(`production.settings.statistics.${item.label}`)}</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StatisticsSettings.propTypes = {
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
statisticsOrder: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
setStatisticsOrder: PropTypes.func.isRequired,
|
||||||
|
setHasChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatisticsSettings;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
const statisticsItems = [
|
||||||
|
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||||
|
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||||
|
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||||
|
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||||
|
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
|
||||||
|
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
|
||||||
|
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
|
||||||
|
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
|
||||||
|
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
||||||
|
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
||||||
|
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
||||||
|
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultKanbanSettings = {
|
||||||
|
ats: true,
|
||||||
|
clm_no: true,
|
||||||
|
compact: false,
|
||||||
|
ownr_nm: true,
|
||||||
|
sublets: true,
|
||||||
|
ins_co_nm: true,
|
||||||
|
production_note: true,
|
||||||
|
employeeassignments: true,
|
||||||
|
scheduled_completion: true,
|
||||||
|
cardcolor: false,
|
||||||
|
orientation: false,
|
||||||
|
tasks: false,
|
||||||
|
cardSize: "small",
|
||||||
|
model_info: true,
|
||||||
|
kiosk: false,
|
||||||
|
totalHrs: true,
|
||||||
|
totalAmountInProduction: false,
|
||||||
|
totalLAB: true,
|
||||||
|
totalLAR: true,
|
||||||
|
jobsInProduction: true,
|
||||||
|
totalHrsOnBoard: false,
|
||||||
|
totalLABOnBoard: false,
|
||||||
|
totalLAROnBoard: false,
|
||||||
|
jobsOnBoard: false,
|
||||||
|
tasksOnBoard: false,
|
||||||
|
tasksInProduction: false,
|
||||||
|
totalAmountOnBoard: true,
|
||||||
|
estimator: false,
|
||||||
|
subtotal: false,
|
||||||
|
statisticsOrder: statisticsItems.map((item) => item.id),
|
||||||
|
selectedMdInsCos: [],
|
||||||
|
selectedEstimators: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeWithDefaults = (settings) => {
|
||||||
|
// Create a new object that starts with the default settings
|
||||||
|
const mergedSettings = { ...defaultKanbanSettings };
|
||||||
|
|
||||||
|
// Override with the provided settings, if any
|
||||||
|
if (settings) {
|
||||||
|
for (const key in settings) {
|
||||||
|
if (settings.hasOwnProperty(key)) {
|
||||||
|
mergedSettings[key] = settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedSettings;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { defaultKanbanSettings, statisticsItems, mergeWithDefaults };
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, Card, Col, Form, notification, Popover, Row, Tabs } from "antd";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||||
|
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||||
|
import LayoutSettings from "./LayoutSettings.jsx";
|
||||||
|
import InformationSettings from "./InformationSettings.jsx";
|
||||||
|
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||||
|
import FilterSettings from "./FilterSettings.jsx";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data }) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [statisticsOrder, setStatisticsOrder] = useState(defaultKanbanSettings.statisticsOrder);
|
||||||
|
const [selectedMdInsCos, setSelectedMdInsCos] = useState(defaultKanbanSettings.selectedMdInsCos);
|
||||||
|
const [selectedEstimators, setSelectedEstimators] = useState(defaultKanbanSettings.selectedEstimators);
|
||||||
|
|
||||||
|
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (associationSettings?.kanban_settings) {
|
||||||
|
const finalSettings = mergeWithDefaults(associationSettings.kanban_settings);
|
||||||
|
form.setFieldsValue(finalSettings);
|
||||||
|
setStatisticsOrder(finalSettings.statisticsOrder);
|
||||||
|
setSelectedMdInsCos(finalSettings.selectedMdInsCos);
|
||||||
|
setSelectedEstimators(finalSettings.selectedEstimators);
|
||||||
|
}
|
||||||
|
}, [form, associationSettings]);
|
||||||
|
|
||||||
|
const handleFinish = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
parentLoading(true);
|
||||||
|
|
||||||
|
const result = await updateKbSettings({
|
||||||
|
variables: {
|
||||||
|
id: associationSettings?.id,
|
||||||
|
ks: {
|
||||||
|
...associationSettings.kanban_settings,
|
||||||
|
...values,
|
||||||
|
statisticsOrder,
|
||||||
|
selectedMdInsCos,
|
||||||
|
selectedEstimators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("production.errors.settings", {
|
||||||
|
error: JSON.stringify(result.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
setLoading(false);
|
||||||
|
parentLoading(false);
|
||||||
|
setHasChanges(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValuesChange = () => setHasChanges(true);
|
||||||
|
|
||||||
|
const handleRestoreDefaults = () => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
...defaultKanbanSettings,
|
||||||
|
statisticsOrder: defaultKanbanSettings.statisticsOrder
|
||||||
|
});
|
||||||
|
setStatisticsOrder(defaultKanbanSettings.statisticsOrder);
|
||||||
|
setSelectedMdInsCos(defaultKanbanSettings.selectedMdInsCos);
|
||||||
|
setSelectedEstimators(defaultKanbanSettings.selectedEstimators);
|
||||||
|
setHasChanges(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const overlay = (
|
||||||
|
<Card style={{ minWidth: "80vw" }}>
|
||||||
|
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="1"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: t("production.settings.layout"),
|
||||||
|
children: <LayoutSettings t={t} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2",
|
||||||
|
label: t("production.settings.information"),
|
||||||
|
children: <InformationSettings t={t} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "3",
|
||||||
|
label: t("production.settings.statistics_title"),
|
||||||
|
children: (
|
||||||
|
<StatisticsSettings
|
||||||
|
t={t}
|
||||||
|
statisticsOrder={statisticsOrder}
|
||||||
|
setStatisticsOrder={setStatisticsOrder}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "4",
|
||||||
|
label: t("production.settings.filters_title"),
|
||||||
|
children: (
|
||||||
|
<FilterSettings
|
||||||
|
selectedMdInsCos={selectedMdInsCos}
|
||||||
|
setSelectedMdInsCos={setSelectedMdInsCos}
|
||||||
|
selectedEstimators={selectedEstimators}
|
||||||
|
setSelectedEstimators={setSelectedEstimators}
|
||||||
|
setHasChanges={setHasChanges}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
|
||||||
|
<Col span={8}>
|
||||||
|
<Button block onClick={() => setOpen(false)}>
|
||||||
|
{t("general.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Button block onClick={handleRestoreDefaults}>
|
||||||
|
{t("general.actions.defaults")}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
<Col span={8}>
|
||||||
|
<Button block onClick={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
|
||||||
|
{t("general.actions.save")}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover content={overlay} open={open} placement="topRight">
|
||||||
|
<Button loading={loading} onClick={() => setOpen(!open)}>
|
||||||
|
{t("production.settings.board_settings")}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductionBoardKanbanSettings.propTypes = {
|
||||||
|
associationSettings: PropTypes.object,
|
||||||
|
parentLoading: PropTypes.func.isRequired,
|
||||||
|
bodyshop: PropTypes.object.isRequired,
|
||||||
|
data: PropTypes.array
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProductionBoardKanbanSettings;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Height Memory Wrapper
|
||||||
|
* @param children
|
||||||
|
* @param maxHeight
|
||||||
|
* @param setMaxHeight
|
||||||
|
* @param override - Override the minHeight style from being set
|
||||||
|
* @param itemKey - Unique key to preserve height for items with the same key
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, itemKey }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const heightMapRef = useRef(new Map());
|
||||||
|
const [localMaxHeight, setLocalMaxHeight] = useState(maxHeight);
|
||||||
|
const [devicePixelRatio, setDevicePixelRatio] = useState(window.devicePixelRatio);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentRef = ref.current;
|
||||||
|
const updateHeight = () => {
|
||||||
|
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||||
|
if (itemKey) {
|
||||||
|
const keyHeight = heightMapRef.current.get(itemKey) || 0;
|
||||||
|
const newHeight = Math.max(keyHeight, currentHeight);
|
||||||
|
heightMapRef.current.set(itemKey, newHeight);
|
||||||
|
setLocalMaxHeight(newHeight);
|
||||||
|
} else {
|
||||||
|
setLocalMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||||
|
}
|
||||||
|
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateHeight);
|
||||||
|
|
||||||
|
if (currentRef?.firstChild) {
|
||||||
|
resizeObserver.observe(currentRef.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeHandler = () => {
|
||||||
|
if (Math.abs(window.devicePixelRatio - devicePixelRatio) > 0.1) {
|
||||||
|
// Threshold to detect significant zoom level change
|
||||||
|
heightMapRef.current.clear(); // Clearing the height memory as zoom level has changed significantly
|
||||||
|
setLocalMaxHeight(0); // Reset local max height
|
||||||
|
setDevicePixelRatio(window.devicePixelRatio); // Update the recorded device pixel ratio
|
||||||
|
}
|
||||||
|
updateHeight();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", resizeHandler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentRef?.firstChild) {
|
||||||
|
resizeObserver.unobserve(currentRef.firstChild);
|
||||||
|
}
|
||||||
|
window.removeEventListener("resize", resizeHandler);
|
||||||
|
};
|
||||||
|
}, [itemKey, setMaxHeight, devicePixelRatio]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (itemKey && heightMapRef.current.has(itemKey)) {
|
||||||
|
setLocalMaxHeight(heightMapRef.current.get(itemKey));
|
||||||
|
}
|
||||||
|
}, [itemKey]);
|
||||||
|
|
||||||
|
const style = override ? {} : { minHeight: localMaxHeight };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} style={style}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
HeightMemoryWrapper.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
maxHeight: PropTypes.number.isRequired,
|
||||||
|
setMaxHeight: PropTypes.func.isRequired,
|
||||||
|
override: PropTypes.bool,
|
||||||
|
itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeightMemoryWrapper;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const HeightPreservingItem = ({ children, ...props }) => {
|
||||||
|
const [size, setSize] = useState(0);
|
||||||
|
const knownSize = props["data-known-size"];
|
||||||
|
useEffect(() => {
|
||||||
|
setSize((prevSize) => {
|
||||||
|
return knownSize === 0 ? prevSize : knownSize;
|
||||||
|
});
|
||||||
|
}, [setSize, knownSize]);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className="height-preserving-container"
|
||||||
|
style={{
|
||||||
|
"--child-height": `${size}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeightPreservingItem;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ItemComponent = ({ children, maxCardHeight, maxCardWidth, ...props }) => (
|
||||||
|
<div style={{ minWidth: maxCardWidth, minHeight: maxCardHeight }} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ItemComponent;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const ItemWrapper = React.memo(({ children, ...props }) => (
|
||||||
|
<div {...props} className="item-wrapper">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default ItemWrapper;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { LaneFooter } from "../styles/Base.js";
|
||||||
|
import { CollapseBtn, ExpandBtn } from "../styles/Elements.js";
|
||||||
|
|
||||||
|
const LaneFooterComponent = ({ onClick, collapsed }) => (
|
||||||
|
<LaneFooter className="react-trello-footer" onClick={onClick}>
|
||||||
|
{collapsed ? <ExpandBtn /> : <CollapseBtn />}
|
||||||
|
</LaneFooter>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default LaneFooterComponent;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React, { forwardRef } from "react";
|
||||||
|
|
||||||
|
const ListComponent = forwardRef(({ style, children, ...props }, ref) => (
|
||||||
|
<div ref={ref} {...props} style={{ ...style }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default ListComponent;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const SizeMemoryWrapper = ({ children, maxHeight, setMaxHeight, maxWidth, setMaxWidth }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentRef = ref.current;
|
||||||
|
|
||||||
|
const updateSize = () => {
|
||||||
|
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||||
|
const currentWidth = currentRef?.firstChild?.clientWidth || 0;
|
||||||
|
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||||
|
setMaxWidth((prevWidth) => Math.max(prevWidth, currentWidth));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateSize);
|
||||||
|
|
||||||
|
if (currentRef?.firstChild) {
|
||||||
|
resizeObserver.observe(currentRef.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoad = () => {
|
||||||
|
if (window.devicePixelRatio < 1) {
|
||||||
|
return; // Do not update width and height
|
||||||
|
}
|
||||||
|
updateSize();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("load", handleLoad);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentRef?.firstChild) {
|
||||||
|
resizeObserver.unobserve(currentRef.firstChild);
|
||||||
|
}
|
||||||
|
window.removeEventListener("load", handleLoad);
|
||||||
|
};
|
||||||
|
}, [setMaxHeight, setMaxWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="size-memory-wrapper" style={{ minHeight: maxHeight, minWidth: maxWidth }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SizeMemoryWrapper.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
maxHeight: PropTypes.number.isRequired,
|
||||||
|
setMaxHeight: PropTypes.func.isRequired,
|
||||||
|
maxWidth: PropTypes.number.isRequired,
|
||||||
|
setMaxWidth: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SizeMemoryWrapper;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { BoardContainer } from "../index";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { StyleHorizontal, StyleVertical } from "../styles/Base.js";
|
||||||
|
import { cardSizesVertical } from "../styles/Globals.js";
|
||||||
|
|
||||||
|
const Board = ({ id, className, orientation, cardSettings, ...additionalProps }) => {
|
||||||
|
const OrientationStyle = useMemo(
|
||||||
|
() => (orientation === "horizontal" ? StyleHorizontal : StyleVertical),
|
||||||
|
[orientation]
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridItemWidth = useMemo(() => {
|
||||||
|
switch (cardSettings?.cardSize) {
|
||||||
|
case "small":
|
||||||
|
return cardSizesVertical.small;
|
||||||
|
case "large":
|
||||||
|
return cardSizesVertical.large;
|
||||||
|
case "medium":
|
||||||
|
return cardSizesVertical.medium;
|
||||||
|
default:
|
||||||
|
return cardSizesVertical.small;
|
||||||
|
}
|
||||||
|
}, [cardSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OrientationStyle {...{ gridItemWidth }}>
|
||||||
|
<BoardContainer
|
||||||
|
orientation={orientation}
|
||||||
|
cardSettings={cardSettings}
|
||||||
|
{...additionalProps}
|
||||||
|
className="react-trello-board"
|
||||||
|
/>
|
||||||
|
</OrientationStyle>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Board;
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { DragDropContext } from "../dnd/lib";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
import Lane from "./Lane";
|
||||||
|
import { PopoverWrapper } from "react-popopo";
|
||||||
|
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||||
|
import { BoardWrapper } from "../styles/Base.js";
|
||||||
|
import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
|
||||||
|
|
||||||
|
const useDragMap = () => {
|
||||||
|
const dragMapRef = useRef(new Map());
|
||||||
|
|
||||||
|
const setDragTime = (laneId) => {
|
||||||
|
dragMapRef.current.set(laneId, Date.now());
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLastDragTime = (laneId) => {
|
||||||
|
return dragMapRef.current.get(laneId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { setDragTime, getLastDragTime };
|
||||||
|
};
|
||||||
|
|
||||||
|
const BoardContainer = ({
|
||||||
|
data,
|
||||||
|
onDataChange = () => {},
|
||||||
|
onDragEnd = () => {},
|
||||||
|
laneSortFunction = () => {},
|
||||||
|
orientation = "horizontal",
|
||||||
|
cardSettings = {},
|
||||||
|
eventBusHandle,
|
||||||
|
reducerData,
|
||||||
|
queryData
|
||||||
|
}) => {
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [maxLaneHeight, setMaxLaneHeight] = useState(0);
|
||||||
|
const [maxCardHeight, setMaxCardHeight] = useState(0);
|
||||||
|
const [maxCardWidth, setMaxCardWidth] = useState(0);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||||
|
const { setDragTime, getLastDragTime } = useDragMap();
|
||||||
|
|
||||||
|
const wireEventBus = useCallback(() => {
|
||||||
|
const eventBus = {
|
||||||
|
publish: (event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
// case "ADD_CARD":
|
||||||
|
// return dispatch(actions.addCard({ laneId: event.laneId, card: event.card }));
|
||||||
|
// case "REMOVE_CARD":
|
||||||
|
// return dispatch(actions.removeCard({ laneId: event.laneId, cardId: event.cardId }));
|
||||||
|
// case "REFRESH_BOARD":
|
||||||
|
// return dispatch(actions.loadBoard(event.data));
|
||||||
|
// case "UPDATE_CARDS":
|
||||||
|
// return dispatch(actions.updateCards({ laneId: event.laneId, cards: event.cards }));
|
||||||
|
// case "UPDATE_CARD":
|
||||||
|
// return dispatch(actions.updateCard({ laneId: event.laneId, updatedCard: event.card }));
|
||||||
|
// case "UPDATE_LANES":
|
||||||
|
// return dispatch(actions.updateLanes(event.lanes));
|
||||||
|
// case "UPDATE_LANE":
|
||||||
|
// return dispatch(actions.updateLane(event.lane));
|
||||||
|
case "MOVE_CARD":
|
||||||
|
return dispatch(
|
||||||
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: event.fromLaneId,
|
||||||
|
toLaneId: event.toLaneId,
|
||||||
|
cardId: event.cardId,
|
||||||
|
index: event.index,
|
||||||
|
event
|
||||||
|
})
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
eventBusHandle(eventBus);
|
||||||
|
}, [dispatch, eventBusHandle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(actions.loadBoard(data));
|
||||||
|
if (eventBusHandle) {
|
||||||
|
wireEventBus();
|
||||||
|
}
|
||||||
|
}, [data, eventBusHandle, dispatch, wireEventBus]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEqual(currentReducerData, reducerData)) {
|
||||||
|
onDataChange(currentReducerData);
|
||||||
|
}
|
||||||
|
}, [currentReducerData, reducerData, onDataChange]);
|
||||||
|
|
||||||
|
const onDragStart = useCallback(() => {
|
||||||
|
setIsDragging(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onLaneDrag = useCallback(
|
||||||
|
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
setDragTime(source.droppableId);
|
||||||
|
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
actions.moveCardAcrossLanes({
|
||||||
|
fromLaneId: source.droppableId,
|
||||||
|
toLaneId: destination.droppableId,
|
||||||
|
cardId: draggableId,
|
||||||
|
index: destination.index
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in onLaneDrag", err);
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch, onDragEnd, setDragTime]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
|
||||||
|
<PopoverWrapper>
|
||||||
|
<BoardWrapper orientation={orientation}>
|
||||||
|
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
|
||||||
|
{currentReducerData.lanes.map((lane, index) => (
|
||||||
|
<Lane
|
||||||
|
key={lane.id}
|
||||||
|
id={lane.id}
|
||||||
|
title={lane.title}
|
||||||
|
index={index}
|
||||||
|
laneSortFunction={laneSortFunction}
|
||||||
|
orientation={orientation}
|
||||||
|
cards={lane.cards}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
cardSettings={cardSettings}
|
||||||
|
maxLaneHeight={maxLaneHeight}
|
||||||
|
setMaxLaneHeight={setMaxLaneHeight}
|
||||||
|
maxCardHeight={maxCardHeight}
|
||||||
|
setMaxCardHeight={setMaxCardHeight}
|
||||||
|
maxCardWidth={maxCardWidth}
|
||||||
|
setMaxCardWidth={setMaxCardWidth}
|
||||||
|
lastDrag={getLastDragTime(lane.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DragDropContext>
|
||||||
|
</BoardWrapper>
|
||||||
|
</PopoverWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BoardContainer.propTypes = {
|
||||||
|
id: PropTypes.string,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
reducerData: PropTypes.object,
|
||||||
|
onDataChange: PropTypes.func,
|
||||||
|
eventBusHandle: PropTypes.func,
|
||||||
|
laneSortFunction: PropTypes.func,
|
||||||
|
handleDragEnd: PropTypes.func,
|
||||||
|
orientation: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoardContainer;
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { bindActionCreators } from "redux";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||||
|
import { Draggable, Droppable } from "../dnd/lib";
|
||||||
|
import { Virtuoso, VirtuosoGrid } from "react-virtuoso";
|
||||||
|
import HeightPreservingItem from "../components/HeightPreservingItem.jsx";
|
||||||
|
import { Section } from "../styles/Base.js";
|
||||||
|
import LaneFooter from "../components/LaneFooter.jsx";
|
||||||
|
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../../../redux/user/user.selectors.js";
|
||||||
|
import { selectTechnician } from "../../../../redux/tech/tech.selectors.js";
|
||||||
|
import ProductionBoardCard from "../../production-board-kanban-card.component.jsx";
|
||||||
|
import HeightMemoryWrapper from "../components/HeightMemoryWrapper.jsx";
|
||||||
|
import SizeMemoryWrapper from "../components/SizeMemoryWrapper.jsx";
|
||||||
|
import ListComponent from "../components/ListComponent.jsx";
|
||||||
|
import ItemComponent from "../components/ItemComponent.jsx";
|
||||||
|
import ItemWrapper from "../components/ItemWrapper.jsx";
|
||||||
|
import objectHash from "object-hash";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lane is a React component that represents a lane in a Trello-like board.
|
||||||
|
* @param id
|
||||||
|
* @param title
|
||||||
|
* @param index
|
||||||
|
* @param isProcessing
|
||||||
|
* @param laneSortFunction
|
||||||
|
* @param cards
|
||||||
|
* @param cardSettings
|
||||||
|
* @param orientation
|
||||||
|
* @param maxLaneHeight
|
||||||
|
* @param setMaxLaneHeight
|
||||||
|
* @param maxCardHeight
|
||||||
|
* @param setMaxCardHeight
|
||||||
|
* @param maxCardWidth
|
||||||
|
* @param setMaxCardWidth
|
||||||
|
* @param lastDrag
|
||||||
|
* @param technician -- connected to redux
|
||||||
|
* @param bodyshop -- connected to redux
|
||||||
|
* @returns {Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const Lane = ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
index,
|
||||||
|
isProcessing,
|
||||||
|
laneSortFunction,
|
||||||
|
cards,
|
||||||
|
cardSettings = {},
|
||||||
|
orientation = "vertical",
|
||||||
|
maxLaneHeight,
|
||||||
|
setMaxLaneHeight,
|
||||||
|
maxCardHeight,
|
||||||
|
setMaxCardHeight,
|
||||||
|
maxCardWidth,
|
||||||
|
setMaxCardWidth,
|
||||||
|
lastDrag,
|
||||||
|
technician,
|
||||||
|
bodyshop
|
||||||
|
}) => {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const laneRef = useRef(null);
|
||||||
|
|
||||||
|
const sortedCards = useMemo(() => {
|
||||||
|
if (!cards) return [];
|
||||||
|
if (!laneSortFunction) return cards;
|
||||||
|
return [...cards].sort(laneSortFunction);
|
||||||
|
}, [cards, laneSortFunction]);
|
||||||
|
|
||||||
|
const toggleLaneCollapsed = useCallback(() => {
|
||||||
|
setCollapsed((prevCollapsed) => !prevCollapsed);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderDraggable = useCallback(
|
||||||
|
(index, card) => {
|
||||||
|
if (!card) {
|
||||||
|
console.log("null card");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={card.id} index={index} key={card.id} isDragDisabled={isProcessing}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
style={provided.draggableProps.style}
|
||||||
|
className={`item ${snapshot.isDragging ? "is-dragging" : ""}`}
|
||||||
|
key={card.id}
|
||||||
|
>
|
||||||
|
<SizeMemoryWrapper
|
||||||
|
maxHeight={maxCardHeight}
|
||||||
|
setMaxHeight={setMaxCardHeight}
|
||||||
|
maxWidth={maxCardWidth}
|
||||||
|
setMaxWidth={setMaxCardWidth}
|
||||||
|
>
|
||||||
|
<ProductionBoardCard
|
||||||
|
technician={technician}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
cardSettings={cardSettings}
|
||||||
|
key={card.id}
|
||||||
|
card={card}
|
||||||
|
style={{ minHeight: maxCardHeight, minWidth: maxCardWidth }}
|
||||||
|
className="react-trello-card"
|
||||||
|
/>
|
||||||
|
</SizeMemoryWrapper>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[isProcessing, technician, bodyshop, cardSettings, maxCardHeight, setMaxCardHeight, maxCardWidth, setMaxCardWidth]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDroppable = useCallback(
|
||||||
|
(provided, renderedCards) => {
|
||||||
|
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
||||||
|
const FinalComponent = collapsed ? "div" : Component;
|
||||||
|
const commonProps = {
|
||||||
|
useWindowScroll: true,
|
||||||
|
data: renderedCards
|
||||||
|
};
|
||||||
|
|
||||||
|
const verticalProps = {
|
||||||
|
...commonProps,
|
||||||
|
listClassName: "grid-container",
|
||||||
|
itemClassName: "grid-item",
|
||||||
|
customScrollParent: laneRef.current,
|
||||||
|
components: {
|
||||||
|
List: ListComponent,
|
||||||
|
Item: ItemComponent
|
||||||
|
},
|
||||||
|
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
||||||
|
overscan: { main: 10, reverse: 10 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const horizontalProps = {
|
||||||
|
...commonProps,
|
||||||
|
components: { Item: HeightPreservingItem },
|
||||||
|
overscan: { main: 3, reverse: 3 },
|
||||||
|
itemContent: (index, item) => renderDraggable(index, item),
|
||||||
|
scrollerRef: provided.innerRef,
|
||||||
|
style: {
|
||||||
|
minWidth: maxCardWidth,
|
||||||
|
minHeight: maxLaneHeight
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
||||||
|
|
||||||
|
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
|
||||||
|
// the lane doesn't shrink when collapsed (in horizontal mode)
|
||||||
|
const finalComponentProps = collapsed
|
||||||
|
? orientation === "horizontal"
|
||||||
|
? {
|
||||||
|
style: {
|
||||||
|
height: maxLaneHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
: componentProps;
|
||||||
|
|
||||||
|
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
|
||||||
|
// a card is dragged over it
|
||||||
|
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeightMemoryWrapper
|
||||||
|
itemKey={objectHash({
|
||||||
|
id,
|
||||||
|
orientation,
|
||||||
|
cardSettings,
|
||||||
|
cardLength: renderedCards?.length
|
||||||
|
})}
|
||||||
|
maxHeight={maxLaneHeight}
|
||||||
|
setMaxHeight={setMaxLaneHeight}
|
||||||
|
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||||
|
style={{ ...provided.droppableProps.style }}
|
||||||
|
>
|
||||||
|
<FinalComponent {...finalComponentProps} />
|
||||||
|
{shouldRenderPlaceholder && provided.placeholder}
|
||||||
|
</div>
|
||||||
|
</HeightMemoryWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[orientation, collapsed, renderDraggable, maxLaneHeight, setMaxLaneHeight, maxCardWidth, id, cardSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDragContainer = useCallback(
|
||||||
|
() => (
|
||||||
|
<Droppable
|
||||||
|
droppableId={id}
|
||||||
|
index={index}
|
||||||
|
type="lane"
|
||||||
|
direction={orientation === "horizontal" ? "vertical" : "grid"}
|
||||||
|
mode="virtual"
|
||||||
|
renderClone={(provided, snapshot, rubric) => {
|
||||||
|
const card = sortedCards[rubric.source.index];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
minHeight: maxCardHeight,
|
||||||
|
minWidth: maxCardWidth
|
||||||
|
}}
|
||||||
|
className={`clone ${snapshot.isDragging ? "is-dragging" : ""}`}
|
||||||
|
key={card.id}
|
||||||
|
>
|
||||||
|
<ProductionBoardCard
|
||||||
|
technician={technician}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
cardSettings={cardSettings}
|
||||||
|
key={card.id}
|
||||||
|
className="react-trello-card"
|
||||||
|
card={card}
|
||||||
|
clone={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(provided) => renderDroppable(provided, sortedCards)}
|
||||||
|
</Droppable>
|
||||||
|
),
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
orientation,
|
||||||
|
renderDroppable,
|
||||||
|
sortedCards,
|
||||||
|
technician,
|
||||||
|
bodyshop,
|
||||||
|
cardSettings,
|
||||||
|
maxCardHeight,
|
||||||
|
maxCardWidth
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section key={`lane-${id}-${lastDrag}`} orientation={orientation} cardSettings={cardSettings}>
|
||||||
|
<div onDoubleClick={toggleLaneCollapsed} className="react-trello-column-header">
|
||||||
|
<span className="lane-title">
|
||||||
|
{collapsed ? <EyeInvisibleOutlined className="icon" /> : <EyeOutlined className="icon" />}
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{renderDragContainer()}
|
||||||
|
<LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Lane.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.node.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
laneSortFunction: PropTypes.func,
|
||||||
|
cards: PropTypes.array.isRequired,
|
||||||
|
orientation: PropTypes.string.isRequired,
|
||||||
|
isProcessing: PropTypes.bool.isRequired,
|
||||||
|
cardSettings: PropTypes.object.isRequired,
|
||||||
|
maxLaneHeight: PropTypes.number.isRequired,
|
||||||
|
setMaxLaneHeight: PropTypes.func.isRequired,
|
||||||
|
maxCardHeight: PropTypes.number.isRequired,
|
||||||
|
setMaxCardHeight: PropTypes.func.isRequired,
|
||||||
|
maxCardWidth: PropTypes.number.isRequired,
|
||||||
|
setMaxCardWidth: PropTypes.func.isRequired,
|
||||||
|
lastDrag: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
actions: bindActionCreators(actions, dispatch)
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
technician: selectTechnician
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Lane);
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { isEqual, origin } from "./state/position";
|
||||||
|
|
||||||
|
export const curves = {
|
||||||
|
outOfTheWay: "cubic-bezier(0.2, 0, 0, 1)",
|
||||||
|
drop: "cubic-bezier(.2,1,.1,1)"
|
||||||
|
};
|
||||||
|
export const combine = {
|
||||||
|
opacity: {
|
||||||
|
// while dropping: fade out totally
|
||||||
|
drop: 0,
|
||||||
|
// while dragging: fade out partially
|
||||||
|
combining: 0.7
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
drop: 0.75
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const timings = {
|
||||||
|
outOfTheWay: 0.2,
|
||||||
|
// greater than the out of the way time
|
||||||
|
// so that when the drop ends everything will
|
||||||
|
// have to be out of the way
|
||||||
|
minDropTime: 0.33,
|
||||||
|
maxDropTime: 0.55
|
||||||
|
};
|
||||||
|
|
||||||
|
// slow timings
|
||||||
|
// uncomment to use
|
||||||
|
// export const timings = {
|
||||||
|
// outOfTheWay: 2,
|
||||||
|
// // greater than the out of the way time
|
||||||
|
// // so that when the drop ends everything will
|
||||||
|
// // have to be out of the way
|
||||||
|
// minDropTime: 3,
|
||||||
|
// maxDropTime: 4,
|
||||||
|
// };
|
||||||
|
|
||||||
|
const outOfTheWayTiming = `${timings.outOfTheWay}s ${curves.outOfTheWay}`;
|
||||||
|
export const placeholderTransitionDelayTime = 0.1;
|
||||||
|
export const transitions = {
|
||||||
|
fluid: `opacity ${outOfTheWayTiming}`,
|
||||||
|
snap: `transform ${outOfTheWayTiming}, opacity ${outOfTheWayTiming}`,
|
||||||
|
drop: (duration) => {
|
||||||
|
const timing = `${duration}s ${curves.drop}`;
|
||||||
|
return `transform ${timing}, opacity ${timing}`;
|
||||||
|
},
|
||||||
|
outOfTheWay: `transform ${outOfTheWayTiming}`,
|
||||||
|
placeholder: `height ${outOfTheWayTiming}, width ${outOfTheWayTiming}, margin ${outOfTheWayTiming}`
|
||||||
|
};
|
||||||
|
const moveTo = (offset) => (isEqual(offset, origin) ? null : `translate(${offset.x}px, ${offset.y}px)`);
|
||||||
|
export const transforms = {
|
||||||
|
moveTo,
|
||||||
|
drop: (offset, isCombining) => {
|
||||||
|
const translate = moveTo(offset);
|
||||||
|
if (!translate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only transforming the translate
|
||||||
|
if (!isCombining) {
|
||||||
|
return translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when dropping while combining we also update the scale
|
||||||
|
return `${translate} scale(${combine.scale.drop})`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const average = (values) => {
|
||||||
|
const sum = values.reduce((previous, current) => previous + current, 0);
|
||||||
|
return sum / values.length;
|
||||||
|
};
|
||||||
|
export default (groupSize) => {
|
||||||
|
console.log("Starting average action timer middleware");
|
||||||
|
console.log(`Will take an average every ${groupSize} actions`);
|
||||||
|
const bucket = {};
|
||||||
|
return () => (next) => (action) => {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = next(action);
|
||||||
|
const end = performance.now();
|
||||||
|
const duration = end - start;
|
||||||
|
if (!bucket[action.type]) {
|
||||||
|
bucket[action.type] = [duration];
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
bucket[action.type].push(duration);
|
||||||
|
if (bucket[action.type].length < groupSize) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
console.warn(`Average time for ${action.type}`, average(bucket[action.type]));
|
||||||
|
|
||||||
|
// reset
|
||||||
|
bucket[action.type] = [];
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import * as timings from "../timings";
|
||||||
|
|
||||||
|
export default () => (next) => (action) => {
|
||||||
|
timings.forceEnable();
|
||||||
|
const key = `redux action: ${action.type}`;
|
||||||
|
timings.start(key);
|
||||||
|
const result = next(action);
|
||||||
|
timings.finish(key);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default (mode = "verbose") =>
|
||||||
|
(store) =>
|
||||||
|
(next) =>
|
||||||
|
(action) => {
|
||||||
|
if (mode === "light") {
|
||||||
|
console.log("🏃 Action:", action.type);
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
console.group(`action: ${action.type}`);
|
||||||
|
console.log("action payload", action.payload);
|
||||||
|
console.log("state before", store.getState());
|
||||||
|
const result = next(action);
|
||||||
|
console.log("state after", store.getState());
|
||||||
|
console.groupEnd();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default () => (next) => (action) => {
|
||||||
|
const title = `👾 redux (action): ${action.type}`;
|
||||||
|
const startMark = `${action.type}:start`;
|
||||||
|
const endMark = `${action.type}:end`;
|
||||||
|
performance.mark(startMark);
|
||||||
|
const result = next(action);
|
||||||
|
performance.mark(endMark);
|
||||||
|
performance.measure(title, startMark, endMark);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
const records = {};
|
||||||
|
let isEnabled = false;
|
||||||
|
const isTimingsEnabled = () => isEnabled;
|
||||||
|
export const forceEnable = () => {
|
||||||
|
isEnabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug: uncomment to enable
|
||||||
|
// forceEnable();
|
||||||
|
|
||||||
|
export const start = (key) => {
|
||||||
|
// we want to strip all the code out for production builds
|
||||||
|
// draw back: can only do timings in dev env (which seems to be fine for now)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
if (!isTimingsEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = performance.now();
|
||||||
|
records[key] = now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const finish = (key) => {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
if (!isTimingsEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const now = performance.now();
|
||||||
|
const previous = records[key];
|
||||||
|
if (!previous) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("cannot finish timing as no previous time found", key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = now - previous;
|
||||||
|
const rounded = result.toFixed(2);
|
||||||
|
const style = (() => {
|
||||||
|
if (result < 12) {
|
||||||
|
return {
|
||||||
|
textColor: "green",
|
||||||
|
symbol: "✅"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (result < 40) {
|
||||||
|
return {
|
||||||
|
textColor: "orange",
|
||||||
|
symbol: "⚠️"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
textColor: "red",
|
||||||
|
symbol: "❌"
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`${style.symbol} %cTiming %c${rounded} %cms %c${key}`,
|
||||||
|
// title
|
||||||
|
"color: blue; font-weight: bold;",
|
||||||
|
// result
|
||||||
|
`color: ${style.textColor}; font-size: 1.1em;`,
|
||||||
|
// ms
|
||||||
|
"color: grey;",
|
||||||
|
// key
|
||||||
|
"color: purple; font-weight: bold;"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const isProduction = import.meta.env.PROD;
|
||||||
|
|
||||||
|
// not replacing newlines (which \s does)
|
||||||
|
const spacesAndTabs = /[ \t]{2,}/g;
|
||||||
|
const lineStartWithSpaces = /^[ \t]*/gm;
|
||||||
|
|
||||||
|
// using .trim() to clear the any newlines before the first text and after last text
|
||||||
|
const clean = (value) => value.replace(spacesAndTabs, " ").replace(lineStartWithSpaces, "").trim();
|
||||||
|
const getDevMessage = (message) =>
|
||||||
|
clean(`
|
||||||
|
%creact-beautiful-dnd
|
||||||
|
|
||||||
|
%c${clean(message)}
|
||||||
|
|
||||||
|
%c👷 This is a development only message. It will be removed in production builds.
|
||||||
|
`);
|
||||||
|
export const getFormattedMessage = (message) => [
|
||||||
|
getDevMessage(message),
|
||||||
|
// title (green400)
|
||||||
|
"color: #00C584; font-size: 1.2em; font-weight: bold;",
|
||||||
|
// message
|
||||||
|
"line-height: 1.5",
|
||||||
|
// footer (purple300)
|
||||||
|
"color: #723874;"
|
||||||
|
];
|
||||||
|
const isDisabledFlag = "__react-beautiful-dnd-disable-dev-warnings";
|
||||||
|
|
||||||
|
export function log(type, message) {
|
||||||
|
// no warnings in production
|
||||||
|
if (isProduction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// manual opt out of warnings
|
||||||
|
if (typeof window !== "undefined" && window[isDisabledFlag]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console[type](...getFormattedMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const warning = log.bind(null, "warn");
|
||||||
|
export const error = log.bind(null, "error");
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export function noop() {}
|
||||||
|
|
||||||
|
export function identity(value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Components
|
||||||
|
export { default as DragDropContext } from "./view/drag-drop-context";
|
||||||
|
export { default as Droppable } from "./view/droppable";
|
||||||
|
export { default as Draggable } from "./view/draggable";
|
||||||
|
|
||||||
|
// Default sensors
|
||||||
|
|
||||||
|
export { useMouseSensor, useTouchSensor, useKeyboardSensor } from "./view/use-sensor-marshal";
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
|
||||||
|
export { resetServerContext } from "./view/drag-drop-context";
|
||||||
|
|
||||||
|
// Public flow types
|
||||||
|
|
||||||
|
// Droppable types
|
||||||
|
|
||||||
|
// Draggable types
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable no-restricted-syntax */
|
||||||
|
const isProduction = import.meta.env.PROD;
|
||||||
|
const prefix = "Invariant failed";
|
||||||
|
|
||||||
|
// Want to use this:
|
||||||
|
// export class RbdInvariant extends Error { }
|
||||||
|
// But it causes babel to bring in a lot of code
|
||||||
|
|
||||||
|
export function RbdInvariant(message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
RbdInvariant.prototype.toString = function toString() {
|
||||||
|
return this.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A copy-paste of tiny-invariant but with a custom error type
|
||||||
|
// Throw an error if the condition fails
|
||||||
|
export function invariant(condition, message) {
|
||||||
|
if (condition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isProduction) {
|
||||||
|
// In production we strip the message but still throw
|
||||||
|
throw new RbdInvariant(prefix);
|
||||||
|
} else {
|
||||||
|
// When not in production we allow the message to pass through
|
||||||
|
// *This block will be removed in production builds*
|
||||||
|
throw new RbdInvariant(`${prefix}: ${message || ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
export function isInteger(value) {
|
||||||
|
if (Number.isInteger) {
|
||||||
|
return Number.isInteger(value);
|
||||||
|
}
|
||||||
|
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using this helper to ensure there are correct flow types
|
||||||
|
// https://github.com/facebook/flow/issues/2221
|
||||||
|
export function values(map) {
|
||||||
|
if (Object.values) {
|
||||||
|
// $FlowFixMe - Object.values currently does not have good flow support
|
||||||
|
return Object.values(map);
|
||||||
|
}
|
||||||
|
return Object.keys(map).map((key) => map[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could also extend to pass index and list
|
||||||
|
|
||||||
|
// TODO: swap order
|
||||||
|
export function findIndex(list, predicate) {
|
||||||
|
if (list.findIndex) {
|
||||||
|
return list.findIndex(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a for loop so that we can exit early
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
if (predicate(list[i])) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Array.prototype.find returns -1 when nothing is found
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function find(list, predicate) {
|
||||||
|
if (list.find) {
|
||||||
|
return list.find(predicate);
|
||||||
|
}
|
||||||
|
const index = findIndex(list, predicate);
|
||||||
|
if (index !== -1) {
|
||||||
|
return list[index];
|
||||||
|
}
|
||||||
|
// Array.prototype.find returns undefined when nothing is found
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using this rather than Array.from as Array.from adds 2kb to the gzip
|
||||||
|
// document.querySelector actually returns Element[], but flow thinks it is HTMLElement[]
|
||||||
|
// So we downcast the result to Element[]
|
||||||
|
export function toArray(list) {
|
||||||
|
return Array.prototype.slice.call(list);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const dragHandleUsageInstructions = `
|
||||||
|
Press space bar to start a drag.
|
||||||
|
When dragging you can use the arrow keys to move the item around and escape to cancel.
|
||||||
|
Some screen readers may require you to be in focus mode or to use your pass through key
|
||||||
|
`;
|
||||||
|
const position = (index) => index + 1;
|
||||||
|
|
||||||
|
// We cannot list what index the Droppable is in automatically as we are not sure how
|
||||||
|
// the Droppable's have been configured
|
||||||
|
const onDragStart = (start) => `
|
||||||
|
You have lifted an item in position ${position(start.source.index)}
|
||||||
|
`;
|
||||||
|
const withLocation = (source, destination) => {
|
||||||
|
const isInHomeList = source.droppableId === destination.droppableId;
|
||||||
|
const startPosition = position(source.index);
|
||||||
|
const endPosition = position(destination.index);
|
||||||
|
if (isInHomeList) {
|
||||||
|
return `
|
||||||
|
You have moved the item from position ${startPosition}
|
||||||
|
to position ${endPosition}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
You have moved the item from position ${startPosition}
|
||||||
|
in list ${source.droppableId}
|
||||||
|
to list ${destination.droppableId}
|
||||||
|
in position ${endPosition}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
const withCombine = (id, source, combine) => {
|
||||||
|
const inHomeList = source.droppableId === combine.droppableId;
|
||||||
|
if (inHomeList) {
|
||||||
|
return `
|
||||||
|
The item ${id}
|
||||||
|
has been combined with ${combine.draggableId}`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
The item ${id}
|
||||||
|
in list ${source.droppableId}
|
||||||
|
has been combined with ${combine.draggableId}
|
||||||
|
in list ${combine.droppableId}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
const onDragUpdate = (update) => {
|
||||||
|
const location = update.destination;
|
||||||
|
if (location) {
|
||||||
|
return withLocation(update.source, location);
|
||||||
|
}
|
||||||
|
const combine = update.combine;
|
||||||
|
if (combine) {
|
||||||
|
return withCombine(update.draggableId, update.source, combine);
|
||||||
|
}
|
||||||
|
return "You are over an area that cannot be dropped on";
|
||||||
|
};
|
||||||
|
const returnedToStart = (source) => `
|
||||||
|
The item has returned to its starting position
|
||||||
|
of ${position(source.index)}
|
||||||
|
`;
|
||||||
|
const onDragEnd = (result) => {
|
||||||
|
if (result.reason === "CANCEL") {
|
||||||
|
return `
|
||||||
|
Movement cancelled.
|
||||||
|
${returnedToStart(result.source)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const location = result.destination;
|
||||||
|
const combine = result.combine;
|
||||||
|
if (location) {
|
||||||
|
return `
|
||||||
|
You have dropped the item.
|
||||||
|
${withLocation(result.source, location)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (combine) {
|
||||||
|
return `
|
||||||
|
You have dropped the item.
|
||||||
|
${withCombine(result.draggableId, result.source, combine)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
The item has been dropped while not over a drop area.
|
||||||
|
${returnedToStart(result.source)}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
const preset = {
|
||||||
|
dragHandleUsageInstructions,
|
||||||
|
onDragStart,
|
||||||
|
onDragUpdate,
|
||||||
|
onDragEnd
|
||||||
|
};
|
||||||
|
export default preset;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
export const beforeInitialCapture = (args) => ({
|
||||||
|
type: "BEFORE_INITIAL_CAPTURE",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const lift = (args) => ({
|
||||||
|
type: "LIFT",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const initialPublish = (args) => ({
|
||||||
|
type: "INITIAL_PUBLISH",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const publishWhileDragging = (args) => ({
|
||||||
|
type: "PUBLISH_WHILE_DRAGGING",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const collectionStarting = () => ({
|
||||||
|
type: "COLLECTION_STARTING",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
export const updateDroppableScroll = (args) => ({
|
||||||
|
type: "UPDATE_DROPPABLE_SCROLL",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const updateDroppableIsEnabled = (args) => ({
|
||||||
|
type: "UPDATE_DROPPABLE_IS_ENABLED",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const updateDroppableIsCombineEnabled = (args) => ({
|
||||||
|
type: "UPDATE_DROPPABLE_IS_COMBINE_ENABLED",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const move = (args) => ({
|
||||||
|
type: "MOVE",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const moveByWindowScroll = (args) => ({
|
||||||
|
type: "MOVE_BY_WINDOW_SCROLL",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const updateViewportMaxScroll = (args) => ({
|
||||||
|
type: "UPDATE_VIEWPORT_MAX_SCROLL",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const moveUp = () => ({
|
||||||
|
type: "MOVE_UP",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
export const moveDown = () => ({
|
||||||
|
type: "MOVE_DOWN",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
export const moveRight = () => ({
|
||||||
|
type: "MOVE_RIGHT",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
export const moveLeft = () => ({
|
||||||
|
type: "MOVE_LEFT",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
export const flush = () => ({
|
||||||
|
type: "FLUSH",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
|
export const animateDrop = (args) => ({
|
||||||
|
type: "DROP_ANIMATE",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const completeDrop = (args) => ({
|
||||||
|
type: "DROP_COMPLETE",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const drop = (args) => ({
|
||||||
|
type: "DROP",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const cancel = () =>
|
||||||
|
drop({
|
||||||
|
reason: "CANCEL"
|
||||||
|
});
|
||||||
|
export const dropPending = (args) => ({
|
||||||
|
type: "DROP_PENDING",
|
||||||
|
payload: args
|
||||||
|
});
|
||||||
|
export const dropAnimationFinished = () => ({
|
||||||
|
type: "DROP_ANIMATION_FINISHED",
|
||||||
|
payload: null
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { add, apply, isEqual, origin } from "../position";
|
||||||
|
|
||||||
|
const smallestSigned = apply((value) => {
|
||||||
|
if (value === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return value > 0 ? 1 : -1;
|
||||||
|
});
|
||||||
|
// We need to figure out how much of the movement
|
||||||
|
// cannot be done with a scroll
|
||||||
|
export const getOverlap = (() => {
|
||||||
|
const getRemainder = (target, max) => {
|
||||||
|
if (target < 0) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
if (target > max) {
|
||||||
|
return target - max;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
return ({ current, max, change }) => {
|
||||||
|
const targetScroll = add(current, change);
|
||||||
|
const overlap = {
|
||||||
|
x: getRemainder(targetScroll.x, max.x),
|
||||||
|
y: getRemainder(targetScroll.y, max.y)
|
||||||
|
};
|
||||||
|
if (isEqual(overlap, origin)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return overlap;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
export const canPartiallyScroll = ({ max: rawMax, current, change }) => {
|
||||||
|
// It is possible for the max scroll to be greater than the current scroll
|
||||||
|
// when there are scrollbars on the cross axis. We adjust for this by
|
||||||
|
// increasing the max scroll point if needed
|
||||||
|
// This will allow movements backwards even if the current scroll is greater than the max scroll
|
||||||
|
const max = {
|
||||||
|
x: Math.max(current.x, rawMax.x),
|
||||||
|
y: Math.max(current.y, rawMax.y)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only need to be able to move the smallest amount in the desired direction
|
||||||
|
const smallestChange = smallestSigned(change);
|
||||||
|
const overlap = getOverlap({
|
||||||
|
max,
|
||||||
|
current,
|
||||||
|
change: smallestChange
|
||||||
|
});
|
||||||
|
|
||||||
|
// no overlap at all - we can move there!
|
||||||
|
if (!overlap) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there was an x value, but there is no x overlap - then we can scroll on the x!
|
||||||
|
if (smallestChange.x !== 0 && overlap.x === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there was an y value, but there is no y overlap - then we can scroll on the y!
|
||||||
|
if (smallestChange.y !== 0 && overlap.y === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
export const canScrollWindow = (viewport, change) =>
|
||||||
|
canPartiallyScroll({
|
||||||
|
current: viewport.scroll.current,
|
||||||
|
max: viewport.scroll.max,
|
||||||
|
change
|
||||||
|
});
|
||||||
|
export const getWindowOverlap = (viewport, change) => {
|
||||||
|
if (!canScrollWindow(viewport, change)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const max = viewport.scroll.max;
|
||||||
|
const current = viewport.scroll.current;
|
||||||
|
return getOverlap({
|
||||||
|
current,
|
||||||
|
max,
|
||||||
|
change
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const canScrollDroppable = (droppable, change) => {
|
||||||
|
const frame = droppable.frame;
|
||||||
|
|
||||||
|
// Cannot scroll when there is no scrollable
|
||||||
|
if (!frame) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return canPartiallyScroll({
|
||||||
|
current: frame.scroll.current,
|
||||||
|
max: frame.scroll.max,
|
||||||
|
change
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const getDroppableOverlap = (droppable, change) => {
|
||||||
|
const frame = droppable.frame;
|
||||||
|
if (!frame) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!canScrollDroppable(droppable, change)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getOverlap({
|
||||||
|
current: frame.scroll.current,
|
||||||
|
max: frame.scroll.max,
|
||||||
|
change
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Values used to control how the fluid auto scroll feels
|
||||||
|
const config = {
|
||||||
|
// percentage distance from edge of container:
|
||||||
|
startFromPercentage: 0.25,
|
||||||
|
maxScrollAtPercentage: 0.05,
|
||||||
|
// pixels per frame
|
||||||
|
maxPixelScroll: 28,
|
||||||
|
// A function used to ease a percentage value
|
||||||
|
// A simple linear function would be: (percentage) => percentage;
|
||||||
|
// percentage is between 0 and 1
|
||||||
|
// result must be between 0 and 1
|
||||||
|
ease: (percentage) => Math.pow(percentage, 2),
|
||||||
|
durationDampening: {
|
||||||
|
// ms: how long to dampen the speed of an auto scroll from the start of a drag
|
||||||
|
stopDampeningAt: 1200,
|
||||||
|
// ms: when to start accelerating the reduction of duration dampening
|
||||||
|
accelerateAt: 360
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { invariant } from "../../../invariant";
|
||||||
|
import isPositionInFrame from "../../visibility/is-position-in-frame";
|
||||||
|
import { toDroppableList } from "../../dimension-structures";
|
||||||
|
import { find } from "../../../native-with-fallback";
|
||||||
|
|
||||||
|
const getScrollableDroppables = memoizeOne((droppables) =>
|
||||||
|
toDroppableList(droppables).filter((droppable) => {
|
||||||
|
// exclude disabled droppables
|
||||||
|
if (!droppable.isEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only want droppables that are scrollable
|
||||||
|
if (!droppable.frame) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const getScrollableDroppableOver = (target, droppables) => {
|
||||||
|
const maybe = find(getScrollableDroppables(droppables), (droppable) => {
|
||||||
|
invariant(droppable.frame, "Invalid result");
|
||||||
|
return isPositionInFrame(droppable.frame.pageMarginBox)(target);
|
||||||
|
});
|
||||||
|
return maybe;
|
||||||
|
};
|
||||||
|
const getBestScrollableDroppable = ({ center, destination, droppables }) => {
|
||||||
|
// We need to scroll the best droppable frame we can so that the
|
||||||
|
// placeholder buffer logic works correctly
|
||||||
|
|
||||||
|
if (destination) {
|
||||||
|
const dimension = droppables[destination];
|
||||||
|
if (!dimension.frame) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If we are not over a droppable - are we over a droppable frame?
|
||||||
|
const dimension = getScrollableDroppableOver(center, droppables);
|
||||||
|
return dimension;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBestScrollableDroppable;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user