- merge master fix conflicts
Signed-off-by: Dave Richer <dave@imexsystems.ca>
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!@#'
|
||||||
@@ -11,4 +11,4 @@ Production-ImEXOnline!@#'
|
|||||||
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||||
|
|
||||||
Generate the license file:
|
Generate the license file:
|
||||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||||
|
|||||||
33
_reference/productionBoardNotes.md
Normal file
33
_reference/productionBoardNotes.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
102
client/package-lock.json
generated
102
client/package-lock.json
generated
@@ -11,7 +11,6 @@
|
|||||||
"@ant-design/icons": "^5.4.0",
|
"@ant-design/icons": "^5.4.0",
|
||||||
"@ant-design/pro-layout": "^7.19.11",
|
"@ant-design/pro-layout": "^7.19.11",
|
||||||
"@apollo/client": "^3.10.8",
|
"@apollo/client": "^3.10.8",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
|
||||||
"@emotion/is-prop-valid": "^1.3.0",
|
"@emotion/is-prop-valid": "^1.3.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
"markerjs2": "^2.32.1",
|
"markerjs2": "^2.32.1",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
@@ -325,18 +325,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asseinfo/react-kanban": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@asseinfo/react-kanban/-/react-kanban-2.2.0.tgz",
|
|
||||||
"integrity": "sha512-/gCigrNXRHeP9VCo8RipTOrA0vAPRIOThJhR4ibVxe6BLkaWFUEuJ1RMT4fODpRRsE3XsdrfVGKkfpRBKgvxXg==",
|
|
||||||
"dependencies": {
|
|
||||||
"react-beautiful-dnd": "^13.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0",
|
|
||||||
"react-dom": "^16.8.0 || ^17.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.24.7",
|
"version": "7.24.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
|
||||||
@@ -5779,25 +5767,6 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-redux": {
|
|
||||||
"version": "7.1.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
|
|
||||||
"integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hoist-non-react-statics": "^3.3.0",
|
|
||||||
"@types/react": "*",
|
|
||||||
"hoist-non-react-statics": "^3.3.0",
|
|
||||||
"redux": "^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-redux/node_modules/redux": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
|
||||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.9.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -13292,6 +13261,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-hash": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||||
@@ -14802,66 +14780,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-beautiful-dnd": {
|
|
||||||
"version": "13.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
|
||||||
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.9.2",
|
|
||||||
"css-box-model": "^1.2.0",
|
|
||||||
"memoize-one": "^5.1.1",
|
|
||||||
"raf-schd": "^4.0.2",
|
|
||||||
"react-redux": "^7.2.0",
|
|
||||||
"redux": "^4.0.4",
|
|
||||||
"use-memo-one": "^1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
|
|
||||||
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-beautiful-dnd/node_modules/memoize-one": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
|
||||||
},
|
|
||||||
"node_modules/react-beautiful-dnd/node_modules/react-is": {
|
|
||||||
"version": "17.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
|
||||||
},
|
|
||||||
"node_modules/react-beautiful-dnd/node_modules/react-redux": {
|
|
||||||
"version": "7.2.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
|
||||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.15.4",
|
|
||||||
"@types/react-redux": "^7.1.20",
|
|
||||||
"hoist-non-react-statics": "^3.3.2",
|
|
||||||
"loose-envify": "^1.4.0",
|
|
||||||
"prop-types": "^15.7.2",
|
|
||||||
"react-is": "^17.0.2"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.3 || ^17 || ^18"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-native": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-beautiful-dnd/node_modules/redux": {
|
|
||||||
"version": "4.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
|
||||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.9.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-big-calendar": {
|
"node_modules/react-big-calendar": {
|
||||||
"version": "1.13.1",
|
"version": "1.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.13.1.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"@ant-design/icons": "^5.4.0",
|
"@ant-design/icons": "^5.4.0",
|
||||||
"@ant-design/pro-layout": "^7.19.11",
|
"@ant-design/pro-layout": "^7.19.11",
|
||||||
"@apollo/client": "^3.10.8",
|
"@apollo/client": "^3.10.8",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
|
||||||
"@emotion/is-prop-valid": "^1.3.0",
|
"@emotion/is-prop-valid": "^1.3.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
@@ -44,6 +43,7 @@
|
|||||||
"markerjs2": "^2.32.1",
|
"markerjs2": "^2.32.1",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ ProductFruitsWrapper.propTypes = {
|
|||||||
currentUser: PropTypes.shape({
|
currentUser: PropTypes.shape({
|
||||||
authorized: PropTypes.bool,
|
authorized: PropTypes.bool,
|
||||||
email: PropTypes.string
|
email: PropTypes.string
|
||||||
}).isRequired,
|
}),
|
||||||
workspaceCode: PropTypes.string.isRequired
|
workspaceCode: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ 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 InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +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 { 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 FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.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",
|
||||||
@@ -47,9 +50,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
children: (
|
children: (
|
||||||
<Row wrap>
|
<Row wrap>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
{!technician ? (
|
||||||
{line.parts_order.order_number}
|
<>
|
||||||
</Link>
|
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||||
|
{line.parts_order.order_number}
|
||||||
|
</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>
|
||||||
@@ -84,17 +93,17 @@ 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}>
|
||||||
<Space>
|
{line.accepted_at ? (
|
||||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
<Space>
|
||||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||||
</Space>
|
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||||
|
</Space>
|
||||||
|
) : null}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)
|
)
|
||||||
@@ -111,6 +120,7 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
<FeatureWrapper featureName="bills" noauth={() => null}>
|
<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
|
||||||
@@ -119,9 +129,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
|||||||
children: (
|
children: (
|
||||||
<Row wrap>
|
<Row wrap>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
{!technician ? (
|
||||||
{line.bill.invoice_number}
|
<>
|
||||||
</Link>
|
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||||
|
{line.bill.invoice_number}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`${line.bill.invoice_number}`
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={4}>
|
<Col span={4}>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -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,19 +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";
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -54,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" }))
|
||||||
});
|
});
|
||||||
@@ -63,6 +65,7 @@ export function JobLinesComponent({
|
|||||||
jobRO,
|
jobRO,
|
||||||
technician,
|
technician,
|
||||||
setPartsOrderContext,
|
setPartsOrderContext,
|
||||||
|
setPartsReceiveContext,
|
||||||
loading,
|
loading,
|
||||||
refetch,
|
refetch,
|
||||||
jobLines,
|
jobLines,
|
||||||
@@ -71,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 {
|
||||||
@@ -198,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",
|
||||||
@@ -213,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',
|
||||||
@@ -322,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}
|
||||||
@@ -337,7 +342,6 @@ export function JobLinesComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
title={t("tasks.buttons.create")}
|
title={t("tasks.buttons.create")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -351,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}
|
||||||
@@ -437,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={
|
||||||
@@ -553,7 +566,7 @@ export function JobLinesComponent({
|
|||||||
>
|
>
|
||||||
{t("joblines.actions.new")}
|
{t("joblines.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
{InstanceRenderManager({ rome: <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,16 +56,49 @@ 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({
|
||||||
title: t("joblines.fields.total"),
|
imex: [
|
||||||
dataIndex: "total",
|
{
|
||||||
key: "total",
|
title: t("joblines.fields.total"),
|
||||||
align: "right",
|
dataIndex: "total",
|
||||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
key: "total",
|
||||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
align: "right",
|
||||||
|
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||||
render: (text, record) => Dinero(record.total).toFormat()
|
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||||
}
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<>
|
||||||
</Form.Item>
|
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_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>
|
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||||
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</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>
|
||||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||||
</Form.Item>
|
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||||
|
</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,6 +18,7 @@ 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";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,33 +1,23 @@
|
|||||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useLazyQuery, 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 React, { useState } from "react";
|
||||||
|
|
||||||
import queryString from "query-string";
|
|
||||||
import React, { useEffect, 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 { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
|
||||||
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 { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
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 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,
|
||||||
@@ -62,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 });
|
||||||
|
|
||||||
@@ -83,42 +60,17 @@ export function PartsOrderListTableComponent({
|
|||||||
sortedInfo: {}
|
sortedInfo: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
const [returnfrombill, setReturnFromBill] = useState();
|
|
||||||
const [billData, setBillData] = useState();
|
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
const selectedpartsorder = search.partsorderid;
|
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
|
||||||
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 : [];
|
||||||
const { refetch } = billsQuery;
|
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) => (
|
const recordActions = (record, showView = false) => (
|
||||||
<Space direction="horizontal" wrap>
|
<Space direction="horizontal" wrap>
|
||||||
{showView && (
|
{showView && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (record.returnfrombill) {
|
|
||||||
setReturnFromBill(record.returnfrombill);
|
|
||||||
} else {
|
|
||||||
setReturnFromBill(null);
|
|
||||||
}
|
|
||||||
handleOnRowClick(record);
|
handleOnRowClick(record);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -298,154 +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={
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<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
|
||||||
@@ -476,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={{
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,196 +6,421 @@ import {
|
|||||||
PauseCircleOutlined
|
PauseCircleOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
|
||||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||||
import "./production-board-card.styles.scss";
|
|
||||||
import dayjs from "../../utils/day";
|
import dayjs from "../../utils/day";
|
||||||
|
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
|
|
||||||
const cardColor = (ssbuckets, totalHrs) => {
|
const cardColor = (ssbuckets, totalHrs) => {
|
||||||
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
|
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 };
|
||||||
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 getContrastYIQ = (bgColor) =>
|
||||||
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
|
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
|
||||||
|
|
||||||
return yiq >= 128 ? "black" : "white";
|
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
|
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>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { metadata } = card;
|
||||||
|
|
||||||
let employee_body, employee_prep, employee_refinish, employee_csr;
|
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
||||||
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 =
|
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
||||||
!!card.scheduled_completion &&
|
return {
|
||||||
((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") ||
|
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
|
||||||
(dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon"));
|
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 totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
|
const pastDueAlert = useMemo(() => {
|
||||||
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
|
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]);
|
||||||
|
|
||||||
|
const headerContent = (
|
||||||
|
<div className="header-content-container">
|
||||||
|
<div className="inner-container">
|
||||||
|
<ProductionAlert
|
||||||
|
record={{
|
||||||
|
id: card.id,
|
||||||
|
production_vars: card?.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} />
|
||||||
|
<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 (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="react-kanban-card imex-kanban-card"
|
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||||
size="small"
|
size="small"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||||
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
color: cardSettings?.cardcolor && contrastYIQ
|
||||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
|
|
||||||
}}
|
}}
|
||||||
title={
|
title={!isBodyEmpty ? headerContent : null}
|
||||||
<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={
|
extra={
|
||||||
<Link to={{ search: `?selected=${card.id}` }}>
|
!isBodyEmpty && (
|
||||||
<EyeFilled />
|
<Link to={{ search: `?selected=${card.id}` }}>
|
||||||
</Link>
|
<EyeFilled />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Row>
|
{isBodyEmpty ? headerContent : bodyContent}
|
||||||
{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>
|
</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,266 +1,240 @@
|
|||||||
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 "@asseinfo/react-kanban/dist/styles.css";
|
|
||||||
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
|
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 { defaultKanbanSettings } 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({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ProductionBoardKanbanComponent({
|
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
|
||||||
data,
|
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
|
||||||
bodyshop,
|
|
||||||
refetch,
|
|
||||||
technician,
|
|
||||||
insertAuditTrail,
|
|
||||||
associationSettings
|
|
||||||
}) {
|
|
||||||
const [boardLanes, setBoardLanes] = useState({
|
|
||||||
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
|
|
||||||
});
|
|
||||||
|
|
||||||
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(() => {
|
||||||
logImEXEvent("kanban_drag_end");
|
if (associationSettings) {
|
||||||
|
setLoading(true);
|
||||||
|
setOrientation(associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [associationSettings]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
setIsMoving(true);
|
setIsMoving(true);
|
||||||
setBoardLanes(moveCard(boardLanes, source, destination));
|
const newBoardData = createBoardData({
|
||||||
|
statuses,
|
||||||
const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
|
data,
|
||||||
const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
|
filter,
|
||||||
const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
|
cardSettings: associationSettings?.kanban_settings
|
||||||
|
|
||||||
const movedCardWillBeFirst = destination.toPosition === 0;
|
|
||||||
|
|
||||||
const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
|
|
||||||
|
|
||||||
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
|
|
||||||
|
|
||||||
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
|
|
||||||
|
|
||||||
const newChildCard = movedCardWillBeLast
|
|
||||||
? null
|
|
||||||
: destinationColumn.cards[
|
|
||||||
sameColumnTransfer
|
|
||||||
? source.fromPosition - destination.toPosition > 0
|
|
||||||
? destination.toPosition
|
|
||||||
: destination.toPosition + 1
|
|
||||||
: destination.toPosition
|
|
||||||
];
|
|
||||||
|
|
||||||
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
|
|
||||||
|
|
||||||
let movedCardNewKanbanParent;
|
|
||||||
if (movedCardWillBeFirst) {
|
|
||||||
//console.log("==> New Card is first.");
|
|
||||||
movedCardNewKanbanParent = "-1";
|
|
||||||
} else if (movedCardWillBeLast) {
|
|
||||||
// console.log("==> New Card is last.");
|
|
||||||
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
|
|
||||||
} else if (!!newChildCard) {
|
|
||||||
// console.log("==> New Card is somewhere in the middle");
|
|
||||||
movedCardNewKanbanParent = newChildCard.kanbanparent;
|
|
||||||
} else {
|
|
||||||
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
|
|
||||||
}
|
|
||||||
const newChildCardNewParent = newChildCard ? card.id : null;
|
|
||||||
const update = await client.mutate({
|
|
||||||
mutation: generate_UPDATE_JOB_KANBAN(
|
|
||||||
oldChildCard ? oldChildCard.id : null,
|
|
||||||
oldChildCardNewParent,
|
|
||||||
card.id,
|
|
||||||
movedCardNewKanbanParent,
|
|
||||||
destination.toColumnId,
|
|
||||||
newChildCard ? newChildCard.id : null,
|
|
||||||
newChildCardNewParent
|
|
||||||
)
|
|
||||||
});
|
|
||||||
insertAuditTrail({
|
|
||||||
jobid: card.id,
|
|
||||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
|
||||||
type: "jobstatuschange"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (update.errors) {
|
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
|
||||||
notification["error"]({
|
...lane,
|
||||||
message: t("production.errors.boardupdate", {
|
title: `${lane.title} (${lane.cards.length})`
|
||||||
message: JSON.stringify(update.errors)
|
}));
|
||||||
})
|
|
||||||
});
|
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 totalHrs = data
|
const onDragEnd = useCallback(
|
||||||
.reduce(
|
async ({ type, source, destination, draggableId }) => {
|
||||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
logImEXEvent("kanban_drag_end");
|
||||||
0
|
|
||||||
)
|
|
||||||
.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 = {
|
if (!type || type !== "lane" || !source || !destination || isMoving) return;
|
||||||
xs: "250",
|
|
||||||
sm: "250",
|
|
||||||
md: "250",
|
|
||||||
lg: "250",
|
|
||||||
xl: "250",
|
|
||||||
xxl: "250"
|
|
||||||
};
|
|
||||||
const compactSizes = {
|
|
||||||
xs: "150",
|
|
||||||
sm: "150",
|
|
||||||
md: "150",
|
|
||||||
lg: "150",
|
|
||||||
xl: "155",
|
|
||||||
xxl: "155"
|
|
||||||
};
|
|
||||||
|
|
||||||
const width = selectedBreakpoint
|
setIsMoving(true);
|
||||||
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
|
|
||||||
? compactSizes[selectedBreakpoint[0]]
|
|
||||||
: standardSizes[selectedBreakpoint[0]]
|
|
||||||
: "250";
|
|
||||||
|
|
||||||
const stickyHeader = {
|
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
|
||||||
renderColumnHeader: ({ title }) => (
|
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
|
||||||
<Sticky>
|
|
||||||
{({
|
|
||||||
style,
|
|
||||||
|
|
||||||
// the following are also available but unused in this example
|
if (!targetLane || !sourceLane) {
|
||||||
isSticky,
|
setIsMoving(false);
|
||||||
wasSticky,
|
console.error("Invalid source or destination lane");
|
||||||
distanceFromTop,
|
return;
|
||||||
distanceFromBottom,
|
}
|
||||||
calculatedHeight
|
|
||||||
}) => (
|
|
||||||
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Sticky>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
const cardSettings =
|
const sameColumnTransfer = source.droppableId === destination.droppableId;
|
||||||
associationSettings &&
|
const sourceCard = getCardByID(boardLanes, draggableId);
|
||||||
associationSettings.kanban_settings &&
|
|
||||||
Object.keys(associationSettings.kanban_settings).length > 0
|
const movedCardWillBeFirst = destination.index === 0;
|
||||||
? associationSettings.kanban_settings
|
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
|
||||||
: {
|
|
||||||
ats: true,
|
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
|
||||||
clm_no: true,
|
const oldChildCard = sourceLane.cards[source.index + 1];
|
||||||
compact: false,
|
|
||||||
ownr_nm: true,
|
const newChildCard = movedCardWillBeLast
|
||||||
sublets: true,
|
? null
|
||||||
ins_co_nm: true,
|
: targetLane.cards[
|
||||||
production_note: true,
|
sameColumnTransfer
|
||||||
employeeassignments: true,
|
? source.index < destination.index
|
||||||
scheduled_completion: true,
|
? destination.index + 1
|
||||||
stickyheader: false,
|
: destination.index
|
||||||
cardcolor: false
|
: destination.index
|
||||||
};
|
];
|
||||||
|
|
||||||
|
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
|
||||||
|
|
||||||
|
let movedCardNewKanbanParent;
|
||||||
|
if (movedCardWillBeFirst) {
|
||||||
|
movedCardNewKanbanParent = "-1";
|
||||||
|
} else if (movedCardWillBeLast) {
|
||||||
|
movedCardNewKanbanParent = lastCardInTargetLane.id;
|
||||||
|
} else if (newChildCard) {
|
||||||
|
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
|
||||||
|
} else {
|
||||||
|
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newChildCardNewParent = newChildCard ? draggableId : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const update = await client.mutate({
|
||||||
|
mutation: generate_UPDATE_JOB_KANBAN(
|
||||||
|
oldChildCard ? oldChildCard.id : null,
|
||||||
|
oldChildCardNewParent,
|
||||||
|
draggableId,
|
||||||
|
movedCardNewKanbanParent,
|
||||||
|
targetLane.id,
|
||||||
|
newChildCard ? newChildCard.id : null,
|
||||||
|
newChildCardNewParent
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: draggableId,
|
||||||
|
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
|
||||||
|
type: "jobstatuschange"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (update.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("production.errors.boardupdate", {
|
||||||
|
message: JSON.stringify(update.errors)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("production.errors.boardupdate", {
|
||||||
|
message: error.message
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsMoving(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
|
||||||
|
);
|
||||||
|
|
||||||
|
const cardSettings = useMemo(
|
||||||
|
() =>
|
||||||
|
associationSettings?.kanban_settings && Object.keys(associationSettings.kanban_settings).length > 0
|
||||||
|
? associationSettings.kanban_settings
|
||||||
|
: defaultKanbanSettings,
|
||||||
|
[associationSettings]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSettingsChange = useCallback((newSettings) => {
|
||||||
|
setLoading(true);
|
||||||
|
setOrientation(newSettings.orientation ? "vertical" : "horizontal");
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Skeleton active />;
|
||||||
|
}
|
||||||
|
|
||||||
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}
|
/>
|
||||||
/>
|
</div>
|
||||||
</StickyContainer>
|
|
||||||
</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,223 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Card, Statistic } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { statisticsItems, defaultKanbanSettings } from "./settings/defaultKanbanSettings.js";
|
||||||
|
export const StatisticType = {
|
||||||
|
HOURS: "hours",
|
||||||
|
AMOUNT: "amount",
|
||||||
|
JOBS: "jobs"
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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 }
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
totalHrs,
|
||||||
|
totalAmountInProduction,
|
||||||
|
totalLAB,
|
||||||
|
totalLAR,
|
||||||
|
jobsInProduction,
|
||||||
|
totalHrsOnBoard,
|
||||||
|
totalAmountOnBoard,
|
||||||
|
totalLABOnBoard,
|
||||||
|
totalLAROnBoard,
|
||||||
|
jobsOnBoard
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
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.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
labhrs: PropTypes.object,
|
||||||
|
larhrs: PropTypes.object,
|
||||||
|
job_totals: PropTypes.object
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
cardSettings: PropTypes.shape({
|
||||||
|
totalHrs: PropTypes.bool,
|
||||||
|
totalLAB: PropTypes.bool,
|
||||||
|
totalLAR: PropTypes.bool,
|
||||||
|
jobsInProduction: PropTypes.bool,
|
||||||
|
totalAmountInProduction: PropTypes.bool,
|
||||||
|
totalHrsOnBoard: PropTypes.bool,
|
||||||
|
totalLABOnBoard: PropTypes.bool,
|
||||||
|
totalLAROnBoard: PropTypes.bool,
|
||||||
|
jobsOnBoard: PropTypes.bool,
|
||||||
|
totalAmountOnBoard: PropTypes.bool,
|
||||||
|
statisticsOrder: PropTypes.arrayOf(PropTypes.number)
|
||||||
|
}).isRequired,
|
||||||
|
reducerData: PropTypes.shape({
|
||||||
|
lanes: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
cards: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
metadata: PropTypes.object
|
||||||
|
})
|
||||||
|
).isRequired
|
||||||
|
})
|
||||||
|
).isRequired
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
.react-kanban-column-header__button {
|
position: relative;
|
||||||
color: #333333;
|
.body-empty-container {
|
||||||
background-color: #ffffff;
|
position: absolute;
|
||||||
border-color: #cccccc;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
.tech-container {
|
||||||
.react-kanban-column-header__button:hover,
|
font-weight: bolder;
|
||||||
.react-kanban-column-header__button:focus,
|
text-align: center;
|
||||||
.react-kanban-column-header__button:active {
|
flex: 1;
|
||||||
background-color: #e6e6e6;
|
.branches-outlined {
|
||||||
}
|
color: orangered;
|
||||||
|
}
|
||||||
.react-kanban-column-adder-button {
|
}
|
||||||
border: 2px dashed #eee;
|
.inner-container {
|
||||||
height: 132px;
|
display: flex;
|
||||||
margin: 5px;
|
align-items: center;
|
||||||
}
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
.react-kanban-column-adder-button:hover {
|
.circle-outline {
|
||||||
cursor: pointer;
|
color: orangered;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.iou-parent {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
|
||||||
return {
|
|
||||||
id: s,
|
|
||||||
title: s,
|
|
||||||
cards: []
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredJobs =
|
const lanes = statuses.map((status) => ({
|
||||||
(search === "" || !search) && !employeeId
|
id: status,
|
||||||
? Jobs
|
title: status,
|
||||||
: Jobs.filter((j) => {
|
cards: []
|
||||||
let include = false;
|
}));
|
||||||
if (search && search !== "") {
|
|
||||||
include = CheckSearch(search, j);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!employeeId) {
|
let filteredJobs =
|
||||||
include =
|
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||||
include ||
|
|
||||||
j.employee_body === employeeId ||
|
|
||||||
j.employee_prep === employeeId ||
|
|
||||||
j.employee_csr === employeeId ||
|
|
||||||
j.employee_refinish === employeeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return include;
|
// Filter jobs by selectedMdInsCos if it has values
|
||||||
});
|
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
||||||
|
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
||||||
|
}
|
||||||
|
|
||||||
const DataGroupedByStatus = groupBy(filteredJobs, (d) => d.status);
|
// Filter jobs by selectedEstimators if it has values
|
||||||
|
if (cardSettings?.selectedEstimators?.length > 0) {
|
||||||
|
filteredJobs = filteredJobs.filter((job) =>
|
||||||
|
cardSettings.selectedEstimators.includes(`${job.est_ct_fn} ${job.est_ct_ln}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
|
const DataGroupedByStatus = groupBy(filteredJobs, "status");
|
||||||
|
|
||||||
|
Object.keys(DataGroupedByStatus).forEach((statusGroupKey) => {
|
||||||
try {
|
try {
|
||||||
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
|
const lane = lanes.find((l) => l.id === statusGroupKey);
|
||||||
if (!needle?.cards) return null;
|
if (!lane) return;
|
||||||
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.log("Error while creating board card", error);
|
console.error("Error while creating board card", error);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return boardLanes;
|
return { lanes };
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckSearch = (search, job) => {
|
// Function to check if a job matches the search and/or employeeId filter
|
||||||
return (
|
const checkFilter = (search, employeeId, job) => {
|
||||||
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
|
const lowerSearch = search?.toLowerCase() ?? "";
|
||||||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
|
|
||||||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
|
const matchesSearch =
|
||||||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
|
lowerSearch &&
|
||||||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
|
[
|
||||||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
job.ro_number,
|
||||||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
job.ownr_fn,
|
||||||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
|
job.ownr_co_nm,
|
||||||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
|
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));
|
||||||
|
|
||||||
|
const matchesEmployeeId =
|
||||||
|
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
|
||||||
|
|
||||||
|
return matchesSearch || matchesEmployeeId;
|
||||||
};
|
};
|
||||||
|
|
||||||
// export const updateBoardOnMove = (board, card, source, destination) => {
|
|
||||||
// //Slice from source
|
|
||||||
|
|
||||||
// const sourceCardList = board.columns.find((x) => x.id === source.fromColumnId)
|
|
||||||
// .cards;
|
|
||||||
// sourceCardList.slice(source.fromPosition, 0);
|
|
||||||
|
|
||||||
// //Splice into destination.
|
|
||||||
// const destCardList = board.columns.find(
|
|
||||||
// (x) => x.id === destination.toColumnId
|
|
||||||
// ).cards;
|
|
||||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
|
||||||
|
|
||||||
// destCardList.splice(destination.toPosition, 0, card);
|
|
||||||
// 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,37 @@
|
|||||||
|
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"
|
||||||
|
].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,46 @@
|
|||||||
|
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" }
|
||||||
|
];
|
||||||
|
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
totalAmountOnBoard: true,
|
||||||
|
estimator: false,
|
||||||
|
subtotal: false,
|
||||||
|
statisticsOrder: statisticsItems.map((item) => item.id),
|
||||||
|
selectedMdInsCos: [],
|
||||||
|
selectedEstimators: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export { defaultKanbanSettings, statisticsItems };
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
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 } from "./defaultKanbanSettings.js";
|
||||||
|
import LayoutSettings from "./LayoutSettings.jsx";
|
||||||
|
import InformationSettings from "./InformationSettings.jsx";
|
||||||
|
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||||
|
import FilterSettings from "./FilterSettings.jsx";
|
||||||
|
|
||||||
|
export default 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) {
|
||||||
|
form.setFieldsValue(associationSettings.kanban_settings);
|
||||||
|
if (associationSettings.kanban_settings.statisticsOrder) {
|
||||||
|
setStatisticsOrder(associationSettings.kanban_settings.statisticsOrder);
|
||||||
|
}
|
||||||
|
if (associationSettings.kanban_settings.selectedMdInsCos) {
|
||||||
|
setSelectedMdInsCos(associationSettings.kanban_settings.selectedMdInsCos);
|
||||||
|
}
|
||||||
|
if (associationSettings.kanban_settings.selectedEstimators) {
|
||||||
|
setSelectedEstimators(associationSettings.kanban_settings.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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/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;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import getScroll from "./get-scroll";
|
||||||
|
import { canScrollDroppable } from "../can-scroll";
|
||||||
|
|
||||||
|
const getDroppableScrollChange = ({ droppable, subject, center, dragStartTime, shouldUseTimeDampening }) => {
|
||||||
|
// We know this has a closestScrollable
|
||||||
|
const frame = droppable.frame;
|
||||||
|
|
||||||
|
// this should never happen - just being safe
|
||||||
|
if (!frame) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const scroll = getScroll({
|
||||||
|
dragStartTime,
|
||||||
|
container: frame.pageMarginBox,
|
||||||
|
subject,
|
||||||
|
center,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
return scroll && canScrollDroppable(droppable, scroll) ? scroll : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDroppableScrollChange;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { warning } from "../../../dev-warning";
|
||||||
|
|
||||||
|
const getPercentage = ({ startOfRange, endOfRange, current }) => {
|
||||||
|
const range = endOfRange - startOfRange;
|
||||||
|
if (range === 0) {
|
||||||
|
warning(`
|
||||||
|
Detected distance range of 0 in the fluid auto scroller
|
||||||
|
This is unexpected and would cause a divide by 0 issue.
|
||||||
|
Not allowing an auto scroll
|
||||||
|
`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const currentInRange = current - startOfRange;
|
||||||
|
|
||||||
|
return currentInRange / range;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getPercentage;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const adjustForSizeLimits = ({ container, subject, proposedScroll }) => {
|
||||||
|
const isTooBigVertically = subject.height > container.height;
|
||||||
|
const isTooBigHorizontally = subject.width > container.width;
|
||||||
|
|
||||||
|
// not too big on any axis
|
||||||
|
if (!isTooBigHorizontally && !isTooBigVertically) {
|
||||||
|
return proposedScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// too big on both axis
|
||||||
|
if (isTooBigHorizontally && isTooBigVertically) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only too big on one axis
|
||||||
|
// Exclude the axis that we cannot scroll on
|
||||||
|
return {
|
||||||
|
x: isTooBigHorizontally ? 0 : proposedScroll.x,
|
||||||
|
y: isTooBigVertically ? 0 : proposedScroll.y
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default adjustForSizeLimits;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import getPercentage from "../../get-percentage";
|
||||||
|
import config from "../../config";
|
||||||
|
import minScroll from "./min-scroll";
|
||||||
|
|
||||||
|
const accelerateAt = config.durationDampening.accelerateAt;
|
||||||
|
const stopAt = config.durationDampening.stopDampeningAt;
|
||||||
|
|
||||||
|
const dampenValueByTime = (proposedScroll, dragStartTime) => {
|
||||||
|
const startOfRange = dragStartTime;
|
||||||
|
const endOfRange = stopAt;
|
||||||
|
const now = Date.now();
|
||||||
|
const runTime = now - startOfRange;
|
||||||
|
|
||||||
|
// we have finished the time dampening period
|
||||||
|
if (runTime >= stopAt) {
|
||||||
|
return proposedScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up to this point we know there is a proposed scroll
|
||||||
|
// but we have not reached our accelerate point
|
||||||
|
// Return the minimum amount of scroll
|
||||||
|
if (runTime < accelerateAt) {
|
||||||
|
return minScroll;
|
||||||
|
}
|
||||||
|
const betweenAccelerateAtAndStopAtPercentage = getPercentage({
|
||||||
|
startOfRange: accelerateAt,
|
||||||
|
endOfRange,
|
||||||
|
current: runTime
|
||||||
|
});
|
||||||
|
const scroll = proposedScroll * config.ease(betweenAccelerateAtAndStopAtPercentage);
|
||||||
|
return Math.ceil(scroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dampenValueByTime;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import config from "../../config";
|
||||||
|
|
||||||
|
const getDistanceThresholds = (container, axis) => {
|
||||||
|
const startScrollingFrom = container[axis.size] * config.startFromPercentage;
|
||||||
|
const maxScrollValueAt = container[axis.size] * config.maxScrollAtPercentage;
|
||||||
|
return {
|
||||||
|
startScrollingFrom,
|
||||||
|
maxScrollValueAt
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// all in pixels
|
||||||
|
|
||||||
|
// converts the percentages in the config into actual pixel values
|
||||||
|
export default getDistanceThresholds;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import getPercentage from "../../get-percentage";
|
||||||
|
import config from "../../config";
|
||||||
|
import minScroll from "./min-scroll";
|
||||||
|
|
||||||
|
const getValueFromDistance = (distanceToEdge, thresholds) => {
|
||||||
|
/*
|
||||||
|
// This function only looks at the distance to one edge
|
||||||
|
// Example: looking at bottom edge
|
||||||
|
|----------------------------------|
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| | => no scroll in this range
|
||||||
|
| |
|
||||||
|
| |
|
||||||
|
| startScrollingFrom (eg 100px) |
|
||||||
|
| |
|
||||||
|
| | => increased scroll value the closer to maxScrollValueAt
|
||||||
|
| maxScrollValueAt (eg 10px) |
|
||||||
|
| | => max scroll value in this range
|
||||||
|
|----------------------------------|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// too far away to auto scroll
|
||||||
|
if (distanceToEdge > thresholds.startScrollingFrom) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use max speed when on or over boundary
|
||||||
|
if (distanceToEdge <= thresholds.maxScrollValueAt) {
|
||||||
|
return config.maxPixelScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when just going on the boundary return the minimum integer
|
||||||
|
if (distanceToEdge === thresholds.startScrollingFrom) {
|
||||||
|
return minScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// to get the % past startScrollingFrom we will calculate
|
||||||
|
// the % the value is from maxScrollValueAt and then invert it
|
||||||
|
const percentageFromMaxScrollValueAt = getPercentage({
|
||||||
|
startOfRange: thresholds.maxScrollValueAt,
|
||||||
|
endOfRange: thresholds.startScrollingFrom,
|
||||||
|
current: distanceToEdge
|
||||||
|
});
|
||||||
|
const percentageFromStartScrollingFrom = 1 - percentageFromMaxScrollValueAt;
|
||||||
|
const scroll = config.maxPixelScroll * config.ease(percentageFromStartScrollingFrom);
|
||||||
|
|
||||||
|
// scroll will always be a positive integer
|
||||||
|
return Math.ceil(scroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getValueFromDistance;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import getValueFromDistance from "./get-value-from-distance";
|
||||||
|
import dampenValueByTime from "./dampen-value-by-time";
|
||||||
|
import minScroll from "./min-scroll";
|
||||||
|
|
||||||
|
const getValue = ({ distanceToEdge, thresholds, dragStartTime, shouldUseTimeDampening }) => {
|
||||||
|
const scroll = getValueFromDistance(distanceToEdge, thresholds);
|
||||||
|
|
||||||
|
// not enough distance to trigger a minimum scroll
|
||||||
|
// we can bail here
|
||||||
|
if (scroll === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dampen an auto scroll speed based on duration of drag
|
||||||
|
|
||||||
|
if (!shouldUseTimeDampening) {
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once we know an auto scroll should occur based on distance,
|
||||||
|
// we must let at least 1px through to trigger a scroll event an
|
||||||
|
// another auto scroll call
|
||||||
|
|
||||||
|
return Math.max(dampenValueByTime(scroll, dragStartTime), minScroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getValue;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import getDistanceThresholds from "./get-distance-thresholds";
|
||||||
|
import getValue from "./get-value";
|
||||||
|
|
||||||
|
const getScrollOnAxis = ({ container, distanceToEdges, dragStartTime, axis, shouldUseTimeDampening }) => {
|
||||||
|
const thresholds = getDistanceThresholds(container, axis);
|
||||||
|
const isCloserToEnd = distanceToEdges[axis.end] < distanceToEdges[axis.start];
|
||||||
|
if (isCloserToEnd) {
|
||||||
|
return getValue({
|
||||||
|
distanceToEdge: distanceToEdges[axis.end],
|
||||||
|
thresholds,
|
||||||
|
dragStartTime,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
-1 *
|
||||||
|
getValue({
|
||||||
|
distanceToEdge: distanceToEdges[axis.start],
|
||||||
|
thresholds,
|
||||||
|
dragStartTime,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getScrollOnAxis;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// A scroll event will only be triggered when there is a value of at least 1px change
|
||||||
|
const minScroll = 1;
|
||||||
|
|
||||||
|
export default minScroll;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { apply, isEqual, origin } from "../../../position";
|
||||||
|
import getScrollOnAxis from "./get-scroll-on-axis";
|
||||||
|
import adjustForSizeLimits from "./adjust-for-size-limits";
|
||||||
|
import { horizontal, vertical } from "../../../axis";
|
||||||
|
|
||||||
|
// will replace -0 and replace with +0
|
||||||
|
const clean = apply((value) => (value === 0 ? 0 : value));
|
||||||
|
|
||||||
|
const getScroll = ({ dragStartTime, container, subject, center, shouldUseTimeDampening }) => {
|
||||||
|
// get distance to each edge
|
||||||
|
const distanceToEdges = {
|
||||||
|
top: center.y - container.top,
|
||||||
|
right: container.right - center.x,
|
||||||
|
bottom: container.bottom - center.y,
|
||||||
|
left: center.x - container.left
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Figure out which x,y values are the best target
|
||||||
|
// 2. Can the container scroll in that direction at all?
|
||||||
|
// If no for both directions, then return null
|
||||||
|
// 3. Is the center close enough to a edge to start a drag?
|
||||||
|
// 4. Based on the distance, calculate the speed at which a scroll should occur
|
||||||
|
// The lower distance value the faster the scroll should be.
|
||||||
|
// Maximum speed value should be hit before the distance is 0
|
||||||
|
// Negative values to not continue to increase the speed
|
||||||
|
const y = getScrollOnAxis({
|
||||||
|
container,
|
||||||
|
distanceToEdges,
|
||||||
|
dragStartTime,
|
||||||
|
axis: vertical,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
const x = getScrollOnAxis({
|
||||||
|
container,
|
||||||
|
distanceToEdges,
|
||||||
|
dragStartTime,
|
||||||
|
axis: horizontal,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
const required = clean({
|
||||||
|
x,
|
||||||
|
y
|
||||||
|
});
|
||||||
|
|
||||||
|
// nothing required
|
||||||
|
if (isEqual(required, origin)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to not scroll in a direction that we are too big to scroll in
|
||||||
|
const limited = adjustForSizeLimits({
|
||||||
|
container,
|
||||||
|
subject,
|
||||||
|
proposedScroll: required
|
||||||
|
});
|
||||||
|
if (!limited) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return isEqual(limited, origin) ? null : limited;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getScroll;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import getScroll from "./get-scroll";
|
||||||
|
import { canScrollWindow } from "../can-scroll";
|
||||||
|
|
||||||
|
const getWindowScrollChange = ({ viewport, subject, center, dragStartTime, shouldUseTimeDampening }) => {
|
||||||
|
const scroll = getScroll({
|
||||||
|
dragStartTime,
|
||||||
|
container: viewport.frame,
|
||||||
|
subject,
|
||||||
|
center,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
return scroll && canScrollWindow(viewport, scroll) ? scroll : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getWindowScrollChange;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import rafSchd from "raf-schd";
|
||||||
|
import scroll from "./scroll";
|
||||||
|
import { invariant } from "../../../invariant";
|
||||||
|
import * as timings from "../../../debug/timings";
|
||||||
|
|
||||||
|
const fluidScroller = ({ scrollWindow, scrollDroppable }) => {
|
||||||
|
const scheduleWindowScroll = rafSchd(scrollWindow);
|
||||||
|
const scheduleDroppableScroll = rafSchd(scrollDroppable);
|
||||||
|
let dragging = null;
|
||||||
|
const tryScroll = (state) => {
|
||||||
|
invariant(dragging, "Cannot fluid scroll if not dragging");
|
||||||
|
const { shouldUseTimeDampening, dragStartTime } = dragging;
|
||||||
|
scroll({
|
||||||
|
state,
|
||||||
|
scrollWindow: scheduleWindowScroll,
|
||||||
|
scrollDroppable: scheduleDroppableScroll,
|
||||||
|
dragStartTime,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const start = (state) => {
|
||||||
|
timings.start("starting fluid scroller");
|
||||||
|
invariant(!dragging, "Cannot start auto scrolling when already started");
|
||||||
|
const dragStartTime = Date.now();
|
||||||
|
let wasScrollNeeded = false;
|
||||||
|
const fakeScrollCallback = () => {
|
||||||
|
wasScrollNeeded = true;
|
||||||
|
};
|
||||||
|
scroll({
|
||||||
|
state,
|
||||||
|
dragStartTime: 0,
|
||||||
|
shouldUseTimeDampening: false,
|
||||||
|
scrollWindow: fakeScrollCallback,
|
||||||
|
scrollDroppable: fakeScrollCallback
|
||||||
|
});
|
||||||
|
dragging = {
|
||||||
|
dragStartTime,
|
||||||
|
shouldUseTimeDampening: wasScrollNeeded
|
||||||
|
};
|
||||||
|
timings.finish("starting fluid scroller");
|
||||||
|
|
||||||
|
// we know an auto scroll is needed - let's do it!
|
||||||
|
if (wasScrollNeeded) {
|
||||||
|
tryScroll(state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const stop = () => {
|
||||||
|
// can be called defensively
|
||||||
|
if (!dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleWindowScroll.cancel();
|
||||||
|
scheduleDroppableScroll.cancel();
|
||||||
|
dragging = null;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
scroll: tryScroll
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fluidScroller;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import getBestScrollableDroppable from "./get-best-scrollable-droppable";
|
||||||
|
import whatIsDraggedOver from "../../droppable/what-is-dragged-over";
|
||||||
|
import getWindowScrollChange from "./get-window-scroll-change";
|
||||||
|
import getDroppableScrollChange from "./get-droppable-scroll-change";
|
||||||
|
const scroll = ({ state, dragStartTime, shouldUseTimeDampening, scrollWindow, scrollDroppable }) => {
|
||||||
|
const center = state.current.page.borderBoxCenter;
|
||||||
|
const draggable = state.dimensions.draggables[state.critical.draggable.id];
|
||||||
|
const subject = draggable.page.marginBox;
|
||||||
|
// 1. Can we scroll the viewport?
|
||||||
|
if (state.isWindowScrollAllowed) {
|
||||||
|
const viewport = state.viewport;
|
||||||
|
const change = getWindowScrollChange({
|
||||||
|
dragStartTime,
|
||||||
|
viewport,
|
||||||
|
subject,
|
||||||
|
center,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
if (change) {
|
||||||
|
scrollWindow(change);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const droppable = getBestScrollableDroppable({
|
||||||
|
center,
|
||||||
|
destination: whatIsDraggedOver(state.impact),
|
||||||
|
droppables: state.dimensions.droppables
|
||||||
|
});
|
||||||
|
if (!droppable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const change = getDroppableScrollChange({
|
||||||
|
dragStartTime,
|
||||||
|
droppable,
|
||||||
|
subject,
|
||||||
|
center,
|
||||||
|
shouldUseTimeDampening
|
||||||
|
});
|
||||||
|
if (change) {
|
||||||
|
scrollDroppable(droppable.descriptor.id, change);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default scroll;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import createFluidScroller from "./fluid-scroller";
|
||||||
|
import createJumpScroller from "./jump-scroller";
|
||||||
|
|
||||||
|
const autoScroller = ({ scrollDroppable, scrollWindow, move }) => {
|
||||||
|
const fluidScroller = createFluidScroller({
|
||||||
|
scrollWindow,
|
||||||
|
scrollDroppable
|
||||||
|
});
|
||||||
|
const jumpScroll = createJumpScroller({
|
||||||
|
move,
|
||||||
|
scrollWindow,
|
||||||
|
scrollDroppable
|
||||||
|
});
|
||||||
|
const scroll = (state) => {
|
||||||
|
// Only allowing auto scrolling in the DRAGGING phase
|
||||||
|
if (state.phase !== "DRAGGING") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.movementMode === "FLUID") {
|
||||||
|
fluidScroller.scroll(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.scrollJumpRequest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jumpScroll(state);
|
||||||
|
};
|
||||||
|
const scroller = {
|
||||||
|
scroll,
|
||||||
|
start: fluidScroller.start,
|
||||||
|
stop: fluidScroller.stop
|
||||||
|
};
|
||||||
|
return scroller;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default autoScroller;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { invariant } from "../../invariant";
|
||||||
|
import { add, subtract } from "../position";
|
||||||
|
import { canScrollWindow, canScrollDroppable, getWindowOverlap, getDroppableOverlap } from "./can-scroll";
|
||||||
|
import whatIsDraggedOver from "../droppable/what-is-dragged-over";
|
||||||
|
|
||||||
|
const jumpScroller = ({ move, scrollDroppable, scrollWindow }) => {
|
||||||
|
const moveByOffset = (state, offset) => {
|
||||||
|
const client = add(state.current.client.selection, offset);
|
||||||
|
move({
|
||||||
|
client
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const scrollDroppableAsMuchAsItCan = (droppable, change) => {
|
||||||
|
// Droppable cannot absorb any of the scroll
|
||||||
|
if (!canScrollDroppable(droppable, change)) {
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
const overlap = getDroppableOverlap(droppable, change);
|
||||||
|
|
||||||
|
// Droppable can absorb the entire change
|
||||||
|
if (!overlap) {
|
||||||
|
scrollDroppable(droppable.descriptor.id, change);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Droppable can only absorb a part of the change
|
||||||
|
const whatTheDroppableCanScroll = subtract(change, overlap);
|
||||||
|
scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll);
|
||||||
|
const remainder = subtract(change, whatTheDroppableCanScroll);
|
||||||
|
return remainder;
|
||||||
|
};
|
||||||
|
const scrollWindowAsMuchAsItCan = (isWindowScrollAllowed, viewport, change) => {
|
||||||
|
if (!isWindowScrollAllowed) {
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
if (!canScrollWindow(viewport, change)) {
|
||||||
|
// window cannot absorb any of the scroll
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
const overlap = getWindowOverlap(viewport, change);
|
||||||
|
|
||||||
|
// window can absorb entire scroll
|
||||||
|
if (!overlap) {
|
||||||
|
scrollWindow(change);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// window can only absorb a part of the scroll
|
||||||
|
const whatTheWindowCanScroll = subtract(change, overlap);
|
||||||
|
scrollWindow(whatTheWindowCanScroll);
|
||||||
|
const remainder = subtract(change, whatTheWindowCanScroll);
|
||||||
|
return remainder;
|
||||||
|
};
|
||||||
|
const jumpScroller = (state) => {
|
||||||
|
const request = state.scrollJumpRequest;
|
||||||
|
if (!request) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const destination = whatIsDraggedOver(state.impact);
|
||||||
|
invariant(destination, "Cannot perform a jump scroll when there is no destination");
|
||||||
|
|
||||||
|
// 1. We scroll the droppable first if we can to avoid the draggable
|
||||||
|
// leaving the list
|
||||||
|
|
||||||
|
const droppableRemainder = scrollDroppableAsMuchAsItCan(state.dimensions.droppables[destination], request);
|
||||||
|
|
||||||
|
// droppable absorbed the entire scroll
|
||||||
|
if (!droppableRemainder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const viewport = state.viewport;
|
||||||
|
const windowRemainder = scrollWindowAsMuchAsItCan(state.isWindowScrollAllowed, viewport, droppableRemainder);
|
||||||
|
|
||||||
|
// window could absorb all the droppable remainder
|
||||||
|
if (!windowRemainder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The entire scroll could not be absorbed by the droppable and window
|
||||||
|
// so we manually move whatever is left
|
||||||
|
moveByOffset(state, windowRemainder);
|
||||||
|
};
|
||||||
|
return jumpScroller;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default jumpScroller;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export const vertical = {
|
||||||
|
direction: "vertical",
|
||||||
|
line: "y",
|
||||||
|
crossAxisLine: "x",
|
||||||
|
start: "top",
|
||||||
|
end: "bottom",
|
||||||
|
size: "height",
|
||||||
|
crossAxisStart: "left",
|
||||||
|
crossAxisEnd: "right",
|
||||||
|
crossAxisSize: "width"
|
||||||
|
};
|
||||||
|
export const horizontal = {
|
||||||
|
direction: "horizontal",
|
||||||
|
line: "x",
|
||||||
|
crossAxisLine: "y",
|
||||||
|
start: "left",
|
||||||
|
end: "right",
|
||||||
|
size: "width",
|
||||||
|
crossAxisStart: "top",
|
||||||
|
crossAxisEnd: "bottom",
|
||||||
|
crossAxisSize: "height"
|
||||||
|
};
|
||||||
|
export const grid = {
|
||||||
|
direction: "horizontal",
|
||||||
|
grid: true,
|
||||||
|
line: "x",
|
||||||
|
crossAxisLine: "y",
|
||||||
|
start: "left",
|
||||||
|
end: "right",
|
||||||
|
size: "width",
|
||||||
|
crossAxisStart: "top",
|
||||||
|
crossAxisEnd: "bottom",
|
||||||
|
crossAxisSize: "height"
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import removeDraggableFromList from "../remove-draggable-from-list";
|
||||||
|
import isHomeOf from "../droppable/is-home-of";
|
||||||
|
import { emptyGroups } from "../no-impact";
|
||||||
|
import { find } from "../../native-with-fallback";
|
||||||
|
import getDisplacementGroups from "../get-displacement-groups";
|
||||||
|
|
||||||
|
function getIndexOfLastItem(draggables, options) {
|
||||||
|
if (!draggables.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const indexOfLastItem = draggables[draggables.length - 1].descriptor.index;
|
||||||
|
|
||||||
|
// When in a foreign list there will be an additional one item in the list
|
||||||
|
return options.inHomeList ? indexOfLastItem : indexOfLastItem + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goAtEnd({ insideDestination, inHomeList, displacedBy, destination }) {
|
||||||
|
const newIndex = getIndexOfLastItem(insideDestination, {
|
||||||
|
inHomeList
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
displaced: emptyGroups,
|
||||||
|
displacedBy,
|
||||||
|
at: {
|
||||||
|
type: "REORDER",
|
||||||
|
destination: {
|
||||||
|
droppableId: destination.descriptor.id,
|
||||||
|
index: newIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function calculateReorderImpact({
|
||||||
|
draggable,
|
||||||
|
insideDestination,
|
||||||
|
destination,
|
||||||
|
viewport,
|
||||||
|
displacedBy,
|
||||||
|
last,
|
||||||
|
index,
|
||||||
|
forceShouldAnimate
|
||||||
|
}) {
|
||||||
|
const inHomeList = isHomeOf(draggable, destination);
|
||||||
|
|
||||||
|
// Go into last spot of list
|
||||||
|
if (index == null) {
|
||||||
|
return goAtEnd({
|
||||||
|
insideDestination,
|
||||||
|
inHomeList,
|
||||||
|
displacedBy,
|
||||||
|
destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// this might be the dragging item
|
||||||
|
const match = find(insideDestination, (item) => item.descriptor.index === index);
|
||||||
|
if (!match) {
|
||||||
|
return goAtEnd({
|
||||||
|
insideDestination,
|
||||||
|
inHomeList,
|
||||||
|
displacedBy,
|
||||||
|
destination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const withoutDragging = removeDraggableFromList(draggable, insideDestination);
|
||||||
|
const sliceFrom = insideDestination.indexOf(match);
|
||||||
|
const impacted = withoutDragging.slice(sliceFrom);
|
||||||
|
const displaced = getDisplacementGroups({
|
||||||
|
afterDragging: impacted,
|
||||||
|
destination,
|
||||||
|
displacedBy,
|
||||||
|
last,
|
||||||
|
viewport: viewport.frame,
|
||||||
|
forceShouldAnimate
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
displaced,
|
||||||
|
displacedBy,
|
||||||
|
at: {
|
||||||
|
type: "REORDER",
|
||||||
|
destination: {
|
||||||
|
droppableId: destination.descriptor.id,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
const canStartDrag = (state, id) => {
|
||||||
|
// Ready to go!
|
||||||
|
if (state.phase === "IDLE") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can lift depending on the type of drop animation
|
||||||
|
if (state.phase !== "DROP_ANIMATING") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - For a user drop we allow the user to drag other Draggables
|
||||||
|
// immediately as items are most likely already in their home
|
||||||
|
// - For a cancel items will be moving back to their original position
|
||||||
|
// as such it is a cleaner experience to block them from dragging until
|
||||||
|
// the drop animation is complete. Otherwise they will be grabbing
|
||||||
|
// items not in their original position which can lead to bad visuals
|
||||||
|
// Not allowing dragging of the dropping draggable
|
||||||
|
if (state.completed.result.draggableId === id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if dropping - allow lifting
|
||||||
|
// if cancelling - disallow lifting
|
||||||
|
return state.completed.result.reason === "DROP";
|
||||||
|
};
|
||||||
|
|
||||||
|
export default canStartDrag;
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import reducer from "./reducer";
|
||||||
|
import lift from "./middleware/lift";
|
||||||
|
import style from "./middleware/style";
|
||||||
|
import drop from "./middleware/drop/drop-middleware";
|
||||||
|
import scrollListener from "./middleware/scroll-listener";
|
||||||
|
import responders from "./middleware/responders/responders-middleware";
|
||||||
|
import dropAnimationFinish from "./middleware/drop/drop-animation-finish-middleware";
|
||||||
|
import dropAnimationFlushOnScroll from "./middleware/drop/drop-animation-flush-on-scroll-middleware";
|
||||||
|
import dimensionMarshalStopper from "./middleware/dimension-marshal-stopper";
|
||||||
|
import focus from "./middleware/focus";
|
||||||
|
import autoScroll from "./middleware/auto-scroll";
|
||||||
|
import pendingDrop from "./middleware/pending-drop";
|
||||||
|
|
||||||
|
const createBoardStore = ({ dimensionMarshal, focusMarshal, styleMarshal, getResponders, announce, autoScroller }) =>
|
||||||
|
configureStore({
|
||||||
|
reducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
//Note: No additional defaults seem required as per original source
|
||||||
|
getDefaultMiddleware({
|
||||||
|
immutableCheck: false,
|
||||||
|
serializableCheck: false,
|
||||||
|
actionCreatorCheck: false,
|
||||||
|
thunk: false
|
||||||
|
}).concat([
|
||||||
|
// ## Debug middleware
|
||||||
|
|
||||||
|
// > uncomment to use
|
||||||
|
// debugging logger
|
||||||
|
// require('../debug/middleware/log').default('light'),
|
||||||
|
// // user timing api
|
||||||
|
// require('../debug/middleware/user-timing').default,
|
||||||
|
// debugging timer
|
||||||
|
// require('../debug/middleware/action-timing').default,
|
||||||
|
// average action timer
|
||||||
|
// require('../debug/middleware/action-timing-average').default(200),
|
||||||
|
|
||||||
|
// ## Application middleware
|
||||||
|
|
||||||
|
// Style updates do not cause more actions. It is important to update styles
|
||||||
|
// before responders are called: specifically the onDragEnd responder. We need to clear
|
||||||
|
// the transition styles off the elements before a reorder to prevent strange
|
||||||
|
// post drag animations in firefox. Even though we clear the transition off
|
||||||
|
// a Draggable - if it is done after a reorder firefox will still apply the
|
||||||
|
// transition.
|
||||||
|
// Must be called before dimension marshal for lifting to apply collecting styles
|
||||||
|
style(styleMarshal),
|
||||||
|
// Stop the dimension marshal collecting anything
|
||||||
|
// when moving into a phase where collection is no longer needed.
|
||||||
|
// We need to stop the marshal before responders fire as responders can cause
|
||||||
|
// dimension registration changes in response to reordering
|
||||||
|
dimensionMarshalStopper(dimensionMarshal),
|
||||||
|
// Fire application responders in response to drag changes
|
||||||
|
lift(dimensionMarshal),
|
||||||
|
drop,
|
||||||
|
// When a drop animation finishes - fire a drop complete
|
||||||
|
dropAnimationFinish,
|
||||||
|
dropAnimationFlushOnScroll,
|
||||||
|
pendingDrop,
|
||||||
|
autoScroll(autoScroller),
|
||||||
|
scrollListener,
|
||||||
|
focus(focusMarshal),
|
||||||
|
// Fire responders for consumers (after update to store)
|
||||||
|
responders(getResponders, announce)
|
||||||
|
]),
|
||||||
|
devTools: import.meta.env.DEV
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createBoardStore;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function didStartAfterCritical(draggableId, afterCritical) {
|
||||||
|
return Boolean(afterCritical.effected[draggableId]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { invariant } from "../../invariant";
|
||||||
|
import createPublisher from "./while-dragging-publisher";
|
||||||
|
import getInitialPublish from "./get-initial-publish";
|
||||||
|
import { warning } from "../../dev-warning";
|
||||||
|
|
||||||
|
function shouldPublishUpdate(registry, dragging, entry) {
|
||||||
|
// do not publish updates for the critical draggable
|
||||||
|
if (entry.descriptor.id === dragging.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// do not publish updates for draggables that are not of a type that we care about
|
||||||
|
if (entry.descriptor.type !== dragging.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const home = registry.droppable.getById(entry.descriptor.droppableId);
|
||||||
|
if (home.descriptor.mode !== "virtual") {
|
||||||
|
warning(`
|
||||||
|
You are attempting to add or remove a Draggable [id: ${entry.descriptor.id}]
|
||||||
|
while a drag is occurring. This is only supported for virtual lists.
|
||||||
|
|
||||||
|
See https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/patterns/virtual-lists.md
|
||||||
|
`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimensionMarashal = (registry, callbacks) => {
|
||||||
|
let collection = null;
|
||||||
|
const publisher = createPublisher({
|
||||||
|
callbacks: {
|
||||||
|
publish: callbacks.publishWhileDragging,
|
||||||
|
collectionStarting: callbacks.collectionStarting
|
||||||
|
},
|
||||||
|
registry
|
||||||
|
});
|
||||||
|
const updateDroppableIsEnabled = (id, isEnabled) => {
|
||||||
|
invariant(
|
||||||
|
registry.droppable.exists(id),
|
||||||
|
`Cannot update is enabled flag of Droppable ${id} as it is not registered`
|
||||||
|
);
|
||||||
|
|
||||||
|
// no need to update the application state if a collection is not occurring
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point a non primary droppable dimension might not yet be published
|
||||||
|
// but may have its enabled state changed. For now we still publish this change
|
||||||
|
// and let the reducer exit early if it cannot find the dimension in the state.
|
||||||
|
callbacks.updateDroppableIsEnabled({
|
||||||
|
id,
|
||||||
|
isEnabled
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const updateDroppableIsCombineEnabled = (id, isCombineEnabled) => {
|
||||||
|
// no need to update
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invariant(
|
||||||
|
registry.droppable.exists(id),
|
||||||
|
`Cannot update isCombineEnabled flag of Droppable ${id} as it is not registered`
|
||||||
|
);
|
||||||
|
callbacks.updateDroppableIsCombineEnabled({
|
||||||
|
id,
|
||||||
|
isCombineEnabled
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const updateDroppableScroll = (id, newScroll) => {
|
||||||
|
// no need to update the application state if a collection is not occurring
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invariant(registry.droppable.exists(id), `Cannot update the scroll on Droppable ${id} as it is not registered`);
|
||||||
|
callbacks.updateDroppableScroll({
|
||||||
|
id,
|
||||||
|
newScroll
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const scrollDroppable = (id, change) => {
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registry.droppable.getById(id).callbacks.scroll(change);
|
||||||
|
};
|
||||||
|
const stopPublishing = () => {
|
||||||
|
// This function can be called defensively
|
||||||
|
if (!collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Stop any pending dom collections or publish
|
||||||
|
publisher.stop();
|
||||||
|
|
||||||
|
// Tell all droppables to stop watching scroll
|
||||||
|
// all good if they where not already listening
|
||||||
|
const home = collection.critical.droppable;
|
||||||
|
registry.droppable.getAllByType(home.type).forEach((entry) => entry.callbacks.dragStopped());
|
||||||
|
|
||||||
|
// Unsubscribe from registry updates
|
||||||
|
collection.unsubscribe();
|
||||||
|
// Finally - clear our collection
|
||||||
|
collection = null;
|
||||||
|
};
|
||||||
|
const subscriber = (event) => {
|
||||||
|
invariant(collection, "Should only be subscribed when a collection is occurring");
|
||||||
|
// The dragging item can be add and removed when using a clone
|
||||||
|
// We do not publish updates for the critical item
|
||||||
|
const dragging = collection.critical.draggable;
|
||||||
|
if (event.type === "ADDITION") {
|
||||||
|
if (shouldPublishUpdate(registry, dragging, event.value)) {
|
||||||
|
publisher.add(event.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.type === "REMOVAL") {
|
||||||
|
if (shouldPublishUpdate(registry, dragging, event.value)) {
|
||||||
|
publisher.remove(event.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const startPublishing = (request) => {
|
||||||
|
invariant(!collection, "Cannot start capturing critical dimensions as there is already a collection");
|
||||||
|
const entry = registry.draggable.getById(request.draggableId);
|
||||||
|
const home = registry.droppable.getById(entry.descriptor.droppableId);
|
||||||
|
const critical = {
|
||||||
|
draggable: entry.descriptor,
|
||||||
|
droppable: home.descriptor
|
||||||
|
};
|
||||||
|
const unsubscribe = registry.subscribe(subscriber);
|
||||||
|
collection = {
|
||||||
|
critical,
|
||||||
|
unsubscribe
|
||||||
|
};
|
||||||
|
return getInitialPublish({
|
||||||
|
critical,
|
||||||
|
registry,
|
||||||
|
scrollOptions: request.scrollOptions
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const marshal = {
|
||||||
|
// Droppable changes
|
||||||
|
updateDroppableIsEnabled,
|
||||||
|
updateDroppableIsCombineEnabled,
|
||||||
|
scrollDroppable,
|
||||||
|
updateDroppableScroll,
|
||||||
|
// Entry
|
||||||
|
startPublishing,
|
||||||
|
stopPublishing
|
||||||
|
};
|
||||||
|
return marshal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dimensionMarashal;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import * as timings from "../../debug/timings";
|
||||||
|
import { toDraggableMap, toDroppableMap } from "../dimension-structures";
|
||||||
|
import getViewport from "../../view/window/get-viewport";
|
||||||
|
|
||||||
|
const getInitialPublish = ({ critical, scrollOptions, registry }) => {
|
||||||
|
const timingKey = "Initial collection from DOM";
|
||||||
|
timings.start(timingKey);
|
||||||
|
const viewport = getViewport();
|
||||||
|
const windowScroll = viewport.scroll.current;
|
||||||
|
const home = critical.droppable;
|
||||||
|
const droppables = registry.droppable
|
||||||
|
.getAllByType(home.type)
|
||||||
|
.map((entry) => entry.callbacks.getDimensionAndWatchScroll(windowScroll, scrollOptions));
|
||||||
|
const draggables = registry.draggable
|
||||||
|
.getAllByType(critical.draggable.type)
|
||||||
|
.map((entry) => entry.getDimension(windowScroll));
|
||||||
|
const dimensions = {
|
||||||
|
draggables: toDraggableMap(draggables),
|
||||||
|
droppables: toDroppableMap(droppables)
|
||||||
|
};
|
||||||
|
timings.finish(timingKey);
|
||||||
|
const result = {
|
||||||
|
dimensions,
|
||||||
|
critical,
|
||||||
|
viewport
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getInitialPublish;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import * as timings from "../../debug/timings";
|
||||||
|
import { origin } from "../position";
|
||||||
|
|
||||||
|
const clean = () => ({
|
||||||
|
additions: {},
|
||||||
|
removals: {},
|
||||||
|
modified: {}
|
||||||
|
});
|
||||||
|
const timingKey = "Publish collection from DOM";
|
||||||
|
export default function createPublisher({ registry, callbacks }) {
|
||||||
|
let staging = clean();
|
||||||
|
let frameId = null;
|
||||||
|
const collect = () => {
|
||||||
|
if (frameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callbacks.collectionStarting();
|
||||||
|
frameId = requestAnimationFrame(() => {
|
||||||
|
frameId = null;
|
||||||
|
timings.start(timingKey);
|
||||||
|
const { additions, removals, modified } = staging;
|
||||||
|
const added = Object.keys(additions)
|
||||||
|
.map(
|
||||||
|
// Using the origin as the window scroll. This will be adjusted when processing the published values
|
||||||
|
(id) => registry.draggable.getById(id).getDimension(origin)
|
||||||
|
)
|
||||||
|
// Dimensions are not guarenteed to be ordered in the same order as keys
|
||||||
|
// So we need to sort them so they are in the correct order
|
||||||
|
.sort((a, b) => a.descriptor.index - b.descriptor.index);
|
||||||
|
const updated = Object.keys(modified).map((id) => {
|
||||||
|
const entry = registry.droppable.getById(id);
|
||||||
|
const scroll = entry.callbacks.getScrollWhileDragging();
|
||||||
|
return {
|
||||||
|
droppableId: id,
|
||||||
|
scroll
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const result = {
|
||||||
|
additions: added,
|
||||||
|
removals: Object.keys(removals),
|
||||||
|
modified: updated
|
||||||
|
};
|
||||||
|
staging = clean();
|
||||||
|
timings.finish(timingKey);
|
||||||
|
callbacks.publish(result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const add = (entry) => {
|
||||||
|
const id = entry.descriptor.id;
|
||||||
|
staging.additions[id] = entry;
|
||||||
|
staging.modified[entry.descriptor.droppableId] = true;
|
||||||
|
if (staging.removals[id]) {
|
||||||
|
delete staging.removals[id];
|
||||||
|
}
|
||||||
|
collect();
|
||||||
|
};
|
||||||
|
const remove = (entry) => {
|
||||||
|
const descriptor = entry.descriptor;
|
||||||
|
staging.removals[descriptor.id] = true;
|
||||||
|
staging.modified[descriptor.droppableId] = true;
|
||||||
|
if (staging.additions[descriptor.id]) {
|
||||||
|
delete staging.additions[descriptor.id];
|
||||||
|
}
|
||||||
|
collect();
|
||||||
|
};
|
||||||
|
const stop = () => {
|
||||||
|
if (!frameId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelAnimationFrame(frameId);
|
||||||
|
frameId = null;
|
||||||
|
staging = clean();
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
add,
|
||||||
|
remove,
|
||||||
|
stop
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { values } from "../native-with-fallback";
|
||||||
|
|
||||||
|
export const toDroppableMap = memoizeOne((droppables) =>
|
||||||
|
droppables.reduce((previous, current) => {
|
||||||
|
previous[current.descriptor.id] = current;
|
||||||
|
return previous;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
export const toDraggableMap = memoizeOne((draggables) =>
|
||||||
|
draggables.reduce((previous, current) => {
|
||||||
|
previous[current.descriptor.id] = current;
|
||||||
|
return previous;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
export const toDroppableList = memoizeOne((droppables) => values(droppables));
|
||||||
|
export const toDraggableList = memoizeOne((draggables) => values(draggables));
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { grid, horizontal, vertical } from "../axis";
|
||||||
|
import { origin } from "../position";
|
||||||
|
import getMaxScroll from "../get-max-scroll";
|
||||||
|
import getSubject from "./util/get-subject";
|
||||||
|
|
||||||
|
const getDroppable = ({ descriptor, isEnabled, isCombineEnabled, isFixedOnPage, direction, client, page, closest }) => {
|
||||||
|
const frame = (() => {
|
||||||
|
if (!closest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { scrollSize, client: frameClient } = closest;
|
||||||
|
|
||||||
|
// scrollHeight and scrollWidth are based on the padding box
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
|
||||||
|
const maxScroll = getMaxScroll({
|
||||||
|
scrollHeight: scrollSize.scrollHeight,
|
||||||
|
scrollWidth: scrollSize.scrollWidth,
|
||||||
|
height: frameClient.paddingBox.height,
|
||||||
|
width: frameClient.paddingBox.width
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
pageMarginBox: closest.page.marginBox,
|
||||||
|
frameClient,
|
||||||
|
scrollSize,
|
||||||
|
shouldClipSubject: closest.shouldClipSubject,
|
||||||
|
scroll: {
|
||||||
|
initial: closest.scroll,
|
||||||
|
current: closest.scroll,
|
||||||
|
max: maxScroll,
|
||||||
|
diff: {
|
||||||
|
value: origin,
|
||||||
|
displacement: origin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
const axis = direction === "vertical" ? vertical : direction === "grid" ? grid : horizontal;
|
||||||
|
const subject = getSubject({
|
||||||
|
page,
|
||||||
|
withPlaceholder: null,
|
||||||
|
axis,
|
||||||
|
frame
|
||||||
|
});
|
||||||
|
const dimension = {
|
||||||
|
descriptor,
|
||||||
|
isCombineEnabled,
|
||||||
|
isFixedOnPage,
|
||||||
|
axis,
|
||||||
|
isEnabled,
|
||||||
|
client,
|
||||||
|
page,
|
||||||
|
frame,
|
||||||
|
subject
|
||||||
|
};
|
||||||
|
return dimension;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getDroppable;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const isHomeOf = (draggable, destination) => draggable.descriptor.droppableId === destination.descriptor.id;
|
||||||
|
|
||||||
|
export default isHomeOf;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user