Merge remote-tracking branch 'origin/master-AIO' into feature/AIO/IO-2776-cdk-fortellis
This commit is contained in:
@@ -2,7 +2,7 @@ NGROK TEsting:
|
||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||
|
||||
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.
|
||||
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!@#'
|
||||
|
||||
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
|
||||
|
||||
41
_reference/productionBoardNotes.md
Normal file
41
_reference/productionBoardNotes.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Production Board Notes:
|
||||
|
||||
## General Notes
|
||||
|
||||
- You can single click the lane footer to collapse/un-collapse the lane
|
||||
- You can double click the lane header to collapse/un-collapse the lane
|
||||
- If you need to scroll horizontally, you can hold shift and use the mouse scroll wheel, or press the mouse scroll wheel while scrolling
|
||||
|
||||
## Board Settings
|
||||
|
||||
#### Layout
|
||||
|
||||
- Board Orientation (Vertical or Horizontal)
|
||||
- This determines the orientation of the card layout on the board.
|
||||
- Horizontal is the default setting, and how the prior board was set up.
|
||||
- Vertical is the new setting and allows lanes to be displayed vertically, with a grid of cards
|
||||
- Card Size (Small, Medium, Large)
|
||||
- This determines the size of the cards on the board.
|
||||
- Small is the default setting, and how the prior board was set up.
|
||||
- Medium and Large are new settings and allow for larger cards to be displayed on the board.
|
||||
- Compact Cards (Tall or Wide)
|
||||
- Formally called 'Compact'
|
||||
- When on, data is displayed on the card vertically
|
||||
- when turned off, some fields may share horizontal space, tightening the card layout
|
||||
- Colored Cards (On or Off)
|
||||
- When on, cards are colored based on the Status color
|
||||
- Kiosk Mode (On or Off)
|
||||
- This should be turned on if the shop is using it on a tablet (Ipad)
|
||||
|
||||
#### Information
|
||||
|
||||
These allow users to turn fields on or off, turning them all off will show the card in the most minimal form
|
||||
|
||||
|
||||
### Statistics
|
||||
|
||||
- The statistics section allows users to see accumulations of both jobs on the board, and jobs in production.
|
||||
- you can click a statistic to turn it on and off, and drag and drop the statistics to rearrange them
|
||||
|
||||
### Filters
|
||||
- Allows you to set, and persist filters for estimators and insurance companies
|
||||
@@ -4,7 +4,7 @@ Clone Repository for:
|
||||
{
|
||||
"name": "node-webhook-scripts",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"main": "index.jsx",
|
||||
"dependencies": {
|
||||
"express": "^4.16.4"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
|
||||
{
|
||||
name: "Bitbucket Webhook",
|
||||
script: "./webhook/index.js",
|
||||
script: "./webhook/index.jsx",
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
||||
VITE_APP_GA_CODE=231099835
|
||||
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
||||
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
||||
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// craco.config.js
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const CracoLessPlugin = require("craco-less");
|
||||
const { convertLegacyToken } = require("@ant-design/compatible/lib");
|
||||
const { theme } = require("antd/lib");
|
||||
|
||||
const { defaultAlgorithm, defaultSeed } = theme;
|
||||
|
||||
const mapToken = defaultAlgorithm(defaultSeed);
|
||||
const v4Token = convertLegacyToken(mapToken);
|
||||
|
||||
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
plugin: CracoLessPlugin,
|
||||
options: {
|
||||
lessLoaderOptions: {
|
||||
lessOptions: {
|
||||
modifyVars: { ...v4Token },
|
||||
javascriptEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
webpack: {
|
||||
configure: (webpackConfig) => {
|
||||
return {
|
||||
...webpackConfig,
|
||||
// Required for Dev Server
|
||||
devServer: {
|
||||
...webpackConfig.devServer,
|
||||
allowedHosts: "all"
|
||||
},
|
||||
optimization: {
|
||||
...webpackConfig.optimization,
|
||||
// Workaround for CircleCI bug caused by the number of CPUs shown
|
||||
// https://github.com/facebook/create-react-app/issues/8320
|
||||
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
||||
if (item instanceof TerserPlugin) {
|
||||
item.options.parallel = 2;
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
devtool: "source-map"
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
|
||||
@@ -1,174 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<link rel="icon" href="/ro-favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<link rel="icon" href="/pm/pm-favicon.ico" />
|
||||
<% } %>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<link rel="icon" href="/ro-favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<link rel="icon" href="/pm/pm-favicon.ico" />
|
||||
<% } %>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#1690ff" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png" />
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#1690ff" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png" />
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<title>ImEX Online</title>
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement('script');
|
||||
s.src = 'https://client.crisp.chat/l.js';
|
||||
s.async = 1;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online" />
|
||||
<title>Rome Online</title>
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<title>ImEX Online</title>
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement('script');
|
||||
s.src = 'https://client.crisp.chat/l.js';
|
||||
s.async = 1;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online" />
|
||||
<title>Rome Online</title>
|
||||
|
||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
||||
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
|
||||
|
||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
animation-style="noanimation"
|
||||
|
||||
party="LiveChat528346"
|
||||
|
||||
minimized-style="bubbleright"
|
||||
|
||||
allow-call="true"
|
||||
|
||||
allow-video="false"
|
||||
|
||||
allow-soundnotifications="true"
|
||||
|
||||
enable-mute="true"
|
||||
|
||||
enable-onmobile="true"
|
||||
|
||||
offline-enabled="true"
|
||||
|
||||
enable="true"
|
||||
|
||||
ignore-queueownership="false"
|
||||
|
||||
authentication="both"
|
||||
|
||||
show-operator-actual-name="true"
|
||||
|
||||
aknowledge-received="true"
|
||||
|
||||
gdpr-enabled="false"
|
||||
|
||||
message-userinfo-format="name"
|
||||
|
||||
message-dateformat="both"
|
||||
|
||||
lang="browser"
|
||||
|
||||
button-icon-type="default"
|
||||
|
||||
greeting-visibility="none"
|
||||
|
||||
greeting-offline-visibility="none"
|
||||
|
||||
chat-delay="2000"
|
||||
|
||||
enable-direct-call="true"
|
||||
|
||||
enable-ga="false"
|
||||
|
||||
></call-us>-->
|
||||
|
||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js id="tcx-callus-js" charset="utf-8"></script>
|
||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
animation-style="noanimation"
|
||||
|
||||
party="LiveChat528346"
|
||||
|
||||
minimized-style="bubbleright"
|
||||
|
||||
allow-call="true"
|
||||
|
||||
allow-video="false"
|
||||
|
||||
allow-soundnotifications="true"
|
||||
|
||||
enable-mute="true"
|
||||
|
||||
enable-onmobile="true"
|
||||
|
||||
offline-enabled="true"
|
||||
|
||||
enable="true"
|
||||
|
||||
ignore-queueownership="false"
|
||||
|
||||
authentication="both"
|
||||
|
||||
show-operator-actual-name="true"
|
||||
|
||||
aknowledge-received="true"
|
||||
|
||||
gdpr-enabled="false"
|
||||
|
||||
message-userinfo-format="name"
|
||||
|
||||
message-dateformat="both"
|
||||
|
||||
lang="browser"
|
||||
|
||||
button-icon-type="default"
|
||||
|
||||
greeting-visibility="none"
|
||||
|
||||
greeting-offline-visibility="none"
|
||||
|
||||
chat-delay="2000"
|
||||
|
||||
enable-direct-call="true"
|
||||
|
||||
enable-ga="false"
|
||||
|
||||
></call-us>-->
|
||||
|
||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js id="tcx-callus-js" charset="utf-8"></script>
|
||||
|
||||
|
||||
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
<meta name="description" content="ProManager" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
<meta name="description" content="ProManager" />
|
||||
|
||||
<% } %>
|
||||
<script>
|
||||
!(function () {
|
||||
'use strict';
|
||||
var e = [
|
||||
'debug',
|
||||
'destroy',
|
||||
'do',
|
||||
'help',
|
||||
'identify',
|
||||
'is',
|
||||
'off',
|
||||
'on',
|
||||
'ready',
|
||||
'render',
|
||||
'reset',
|
||||
'safe',
|
||||
'set',
|
||||
];
|
||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement('script');
|
||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
<% } %>
|
||||
<script>
|
||||
!(function () {
|
||||
'use strict';
|
||||
var e = [
|
||||
'debug',
|
||||
'destroy',
|
||||
'do',
|
||||
'help',
|
||||
'identify',
|
||||
'is',
|
||||
'off',
|
||||
'on',
|
||||
'ready',
|
||||
'render',
|
||||
'reset',
|
||||
'safe',
|
||||
'set',
|
||||
];
|
||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement('script');
|
||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22372
client/package-lock.json
generated
22372
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,90 +2,92 @@
|
||||
"name": "bodyshop",
|
||||
"version": "0.2.1",
|
||||
"engines": {
|
||||
"node": "18.18.2"
|
||||
"node": ">=18.18.2"
|
||||
},
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/compatible": "^5.1.2",
|
||||
"@ant-design/pro-layout": "^7.17.16",
|
||||
"@apollo/client": "^3.8.10",
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.2.2",
|
||||
"@ant-design/pro-layout": "^7.19.11",
|
||||
"@apollo/client": "^3.10.8",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"@sentry/cli": "^2.28.6",
|
||||
"@sentry/react": "^7.104.0",
|
||||
"@splitsoftware/splitio-react": "^1.11.0",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@sentry/cli": "^2.32.2",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.0",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.15.3",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.19.3",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.6.8",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.8.1",
|
||||
"graphql": "^16.6.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"libphonenumber-js": "^1.10.57",
|
||||
"logrocket": "^8.0.1",
|
||||
"markerjs2": "^2.32.0",
|
||||
"normalize-url": "^8.0.0",
|
||||
"firebase": "^10.12.4",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.11.4",
|
||||
"logrocket": "^8.1.1",
|
||||
"markerjs2": "^2.32.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-big-calendar": "^1.11.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-cookie": "^7.1.4",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^14.0.5",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-joyride": "^2.7.4",
|
||||
"react-joyride": "^2.8.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.3.3",
|
||||
"react-number-format": "^5.4.0",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.6",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"recharts": "^2.12.2",
|
||||
"react-virtuoso": "^4.7.12",
|
||||
"recharts": "^2.12.7",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.0",
|
||||
"sass": "^1.71.1",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"styled-components": "^6.1.8",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.77.8",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.12",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"userpilot": "^1.3.1",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.2",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2",
|
||||
"workbox-core": "^7.0.0",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
"workbox-navigation-preload": "^7.0.0",
|
||||
"workbox-precaching": "^7.0.0",
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0"
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
||||
@@ -128,32 +130,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@dotenvx/dotenvx": "^0.15.4",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@sentry/webpack-plugin": "^2.14.2",
|
||||
"@swc/core": "^1.3.107",
|
||||
"@swc/plugin-styled-components": "^1.5.108",
|
||||
"@testing-library/cypress": "^10.0.1",
|
||||
"browserslist": "^4.22.3",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@dotenvx/dotenvx": "^1.6.4",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/react": "^11.12.0",
|
||||
"@sentry/webpack-plugin": "^2.21.1",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.23.2",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"cypress": "^13.13.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.6.0",
|
||||
"memfs": "^4.9.3",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-style-import": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method
|
||||
$ npm install --save-dev stubs
|
||||
```
|
||||
```js
|
||||
var mylib = require('./lib/index.js')
|
||||
var mylib = require('./lib/index.jsx')
|
||||
var stubs = require('stubs')
|
||||
|
||||
// make it a noop
|
||||
|
||||
@@ -16567,7 +16567,7 @@ even more slower.
|
||||
## Benchmarks
|
||||
|
||||
```bash
|
||||
$ node benchmarks/index.js
|
||||
$ node benchmarks/index.jsx
|
||||
Benchmarking: sign
|
||||
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
||||
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
||||
|
||||
56
client/public/firebase-messaging-sw.js
Normal file
56
client/public/firebase-messaging-sw.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Scripts for firebase and firebase messaging
|
||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
|
||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
|
||||
|
||||
// Initialize the Firebase app in the service worker by passing the generated config
|
||||
let firebaseConfig;
|
||||
switch (this.location.hostname) {
|
||||
case "localhost":
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc",
|
||||
authDomain: "imex-dev.firebaseapp.com",
|
||||
databaseURL: "https://imex-dev.firebaseio.com",
|
||||
projectId: "imex-dev",
|
||||
storageBucket: "imex-dev.appspot.com",
|
||||
messagingSenderId: "759548147434",
|
||||
appId: "1:759548147434:web:e8239868a48ceb36700993",
|
||||
measurementId: "G-K5XRBVVB4S",
|
||||
};
|
||||
break;
|
||||
case "test.imex.online":
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c",
|
||||
authDomain: "imex-test.firebaseapp.com",
|
||||
projectId: "imex-test",
|
||||
storageBucket: "imex-test.appspot.com",
|
||||
messagingSenderId: "991923618608",
|
||||
appId: "1:991923618608:web:633437569cdad78299bef5",
|
||||
// measurementId: "${config.measurementId}",
|
||||
};
|
||||
break;
|
||||
case "imex.online":
|
||||
default:
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU",
|
||||
authDomain: "imex-prod.firebaseapp.com",
|
||||
databaseURL: "https://imex-prod.firebaseio.com",
|
||||
projectId: "imex-prod",
|
||||
storageBucket: "imex-prod.appspot.com",
|
||||
messagingSenderId: "253497221485",
|
||||
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
||||
measurementId: "G-NTWBKG2L0M",
|
||||
};
|
||||
}
|
||||
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
|
||||
// Retrieve firebase messaging
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
messaging.onBackgroundMessage(function (payload) {
|
||||
// Customize notification here
|
||||
const channel = new BroadcastChannel("imex-sw-messages");
|
||||
channel.postMessage(payload);
|
||||
|
||||
//self.registration.showNotification(notificationTitle, notificationOptions);
|
||||
});
|
||||
@@ -7,9 +7,7 @@ import { connect } from "react-redux";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||
|
||||
//Component Imports
|
||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
|
||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
||||
import LandingPage from "../pages/landing/landing.page";
|
||||
@@ -23,20 +21,21 @@ import "./App.styles.scss";
|
||||
import handleBeta from "../utils/betaHandler";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||
|
||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
online: selectOnline,
|
||||
bodyshop: selectBodyshop,
|
||||
currentEula: selectCurrentEula
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkUserSession: () => dispatch(checkUserSession()),
|
||||
setOnline: (isOnline) => dispatch(setOnline(isOnline))
|
||||
@@ -60,11 +59,11 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
|
||||
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||
useEffect(() => {
|
||||
const offlineListener = (e) => {
|
||||
const offlineListener = () => {
|
||||
setOnline(false);
|
||||
};
|
||||
|
||||
const onlineListener = (e) => {
|
||||
const onlineListener = () => {
|
||||
setOnline(true);
|
||||
};
|
||||
|
||||
@@ -98,7 +97,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
InstanceRenderMgr({
|
||||
imex: "gvfvfw/bodyshopapp",
|
||||
rome: "rome-online/rome-online",
|
||||
promanager: "" //TODO:AIO Add in log rocket for promanager instances.
|
||||
promanager: "" // TODO: AIO Add in log rocket for promanager instances.
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -111,24 +110,20 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
|
||||
handleBeta();
|
||||
|
||||
if (!online)
|
||||
if (!online) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.nointernet")}
|
||||
subTitle={t("general.labels.nointernet_sub")}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<Button type="primary" onClick={() => window.location.reload()}>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||
return <Eula />;
|
||||
@@ -147,18 +142,13 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ProductFruits
|
||||
<ProductFruitsWrapper
|
||||
currentUser={currentUser}
|
||||
workspaceCode={InstanceRenderMgr({
|
||||
imex: null,
|
||||
rome: null,
|
||||
rome: "9BkbEseqNqxw8jUH",
|
||||
promanager: "aoJoEifvezYI0Z0P"
|
||||
})}
|
||||
debug
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
}}
|
||||
/>
|
||||
|
||||
<Routes>
|
||||
|
||||
@@ -161,3 +161,15 @@
|
||||
.rowWithColor > td {
|
||||
background-color: var(--bgColor) !important;
|
||||
}
|
||||
|
||||
.muted-button {
|
||||
color: lightgray;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px; /* Adjust as needed */
|
||||
}
|
||||
|
||||
.muted-button:hover {
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
32
client/src/App/ProductFruitsWrapper.jsx
Normal file
32
client/src/App/ProductFruitsWrapper.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
return (
|
||||
workspaceCode &&
|
||||
currentUser?.authorized === true &&
|
||||
currentUser?.email && (
|
||||
<ProductFruits
|
||||
lifeCycle="unmount"
|
||||
workspaceCode={workspaceCode}
|
||||
debug
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export default ProductFruitsWrapper;
|
||||
|
||||
ProductFruitsWrapper.propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../utils/day";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||
@@ -22,7 +23,6 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
|
||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -153,6 +153,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -188,7 +189,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
}
|
||||
/>
|
||||
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse} />
|
||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
|
||||
@@ -41,7 +41,8 @@ export function BillFormComponent({
|
||||
job,
|
||||
loadOutstandingReturns,
|
||||
loadInventory,
|
||||
preferredMake
|
||||
preferredMake,
|
||||
disableInHouse
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -177,7 +178,7 @@ export function BillFormComponent({
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
disabled={disabled}
|
||||
disabled={disabled || disableInHouse}
|
||||
options={vendorAutoCompleteOptions}
|
||||
preferredMake={preferredMake}
|
||||
onSelect={handleVendorSelect}
|
||||
@@ -243,7 +244,7 @@ export function BillFormComponent({
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled || disableInvNumber} />
|
||||
<Input disabled={disabled || disableInvNumber || disableInHouse} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bills.fields.date")}
|
||||
|
||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
|
||||
const {
|
||||
treatments: { Simple_Inventory }
|
||||
} = useSplitTreatments({
|
||||
@@ -47,6 +47,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
|
||||
job={lineData ? lineData.jobs_by_pk : null}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
disableInHouse={disableInHouse}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
loadInventory={loadInventory}
|
||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||
|
||||
@@ -3,8 +3,6 @@ import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||
|
||||
//To be used as a form element only.
|
||||
const { Option } = Select;
|
||||
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -25,31 +23,27 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
);
|
||||
}}
|
||||
notFoundContent={"Removed."}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
{t("billlines.labels.other")}
|
||||
</Select.Option>
|
||||
{options
|
||||
? options.map((item) => (
|
||||
<Option
|
||||
disabled={allowRemoved ? false : item.removed}
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
cost={item.act_price ? item.act_price : 0}
|
||||
part_type={item.part_type}
|
||||
line_desc={item.line_desc}
|
||||
part_qty={item.part_qty}
|
||||
oem_partno={item.oem_partno}
|
||||
alt_partno={item.alt_partno}
|
||||
act_price={item.act_price}
|
||||
style={{
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
}}
|
||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||
>
|
||||
options={[
|
||||
{ value: "noline", label: t("billlines.labels.other"), name: t("billlines.labels.other") },
|
||||
...options.map((item) => ({
|
||||
disabled: allowRemoved ? false : item.removed,
|
||||
key: item.id,
|
||||
value: item.id,
|
||||
cost: item.act_price ? item.act_price : 0,
|
||||
part_type: item.part_type,
|
||||
line_desc: item.line_desc,
|
||||
part_qty: item.part_qty,
|
||||
oem_partno: item.oem_partno,
|
||||
alt_partno: item.alt_partno,
|
||||
act_price: item.act_price,
|
||||
style: {
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
},
|
||||
name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||
label: (
|
||||
<>
|
||||
<span>
|
||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
@@ -60,14 +54,15 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
|
||||
)
|
||||
})}
|
||||
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
||||
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||
</span>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
}))
|
||||
]}
|
||||
{...restProps}
|
||||
></Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(BillLineSearchSelect);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
@@ -28,12 +26,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const { context } = cardPaymentModal;
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -42,7 +40,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
skip: true
|
||||
});
|
||||
|
||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
@@ -51,16 +48,20 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||
//Add a slight delay to allow the refetch to properly get the data.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
|
||||
const { payments } = form.getFieldsValue();
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: payments.map((payment) => ({
|
||||
@@ -85,50 +86,9 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: values.payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||
payer: t("payments.labels.customer"),
|
||||
type: values.paymentResponse.cardbrand,
|
||||
jobid: payment.jobid,
|
||||
date: dayjs(Date.now()),
|
||||
payment_responses: {
|
||||
data: [
|
||||
{
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
|
||||
jobid: payment.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"]
|
||||
});
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("payments.errors.inserting", { error: error.message })
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
@@ -140,7 +100,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
try {
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -169,7 +130,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
@@ -246,18 +206,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
console.log("Updating the owner info section.");
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
console.log("**Calling refetch.");
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
console.log(
|
||||
"Acc info",
|
||||
data,
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
@@ -300,6 +256,13 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
type="hidden"
|
||||
value={totalAmountToCharge?.toFixed(2)}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="comment"
|
||||
type="hidden"
|
||||
value={btoa(JSON.stringify(payments))}
|
||||
hidden
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
@@ -314,11 +277,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
@@ -28,6 +28,7 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn"
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.unavailable"),
|
||||
value: "courtesycars.status.unavailable",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => record.status === value,
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
|
||||
@@ -9,7 +9,6 @@ import axios from "axios";
|
||||
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
|
||||
|
||||
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
|
||||
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lifecycleData, setLifecycleData] = useState(null);
|
||||
@@ -143,7 +142,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
||||
>
|
||||
<div>
|
||||
{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
|
||||
aria-label={`${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"
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.status}
|
||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -89,8 +89,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
console.log("Render record out today");
|
||||
console.dir(record);
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -22,6 +21,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { determineDmsType } from "../../pages/dms/dms.container";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import i18n from "../../translations/i18n";
|
||||
import dayjs from "../../utils/day";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
@@ -89,7 +89,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
job.area_of_damage && job.area_of_damage.impact1
|
||||
? " " +
|
||||
t("jobs.labels.dms.damageto", {
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN"
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
|
||||
})
|
||||
: ""
|
||||
}`.slice(0, 239),
|
||||
|
||||
@@ -3,14 +3,13 @@ import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(false);
|
||||
|
||||
@@ -178,15 +177,7 @@ export default function GlobalSearchOs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={data}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history(opt.label.props.to);
|
||||
}}
|
||||
onClear={() => setData([])}
|
||||
>
|
||||
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -12,7 +12,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
||||
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
@@ -157,14 +156,7 @@ export default function GlobalSearch() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history(opt.label.props.to);
|
||||
}}
|
||||
>
|
||||
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -32,6 +32,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
import { RiSurveyLine } from "react-icons/ri";
|
||||
@@ -42,7 +43,6 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
@@ -141,11 +141,13 @@ function Header({
|
||||
accountingChildren.push(
|
||||
{
|
||||
key: "bills",
|
||||
id: "header-accounting-bills",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterbills",
|
||||
id: "header-accounting-enterbills",
|
||||
icon: <Icon component={GiPayMoney} />,
|
||||
label: t("menus.header.enterbills"),
|
||||
onClick: () => {
|
||||
@@ -165,6 +167,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "inventory",
|
||||
id: "header-accounting-inventory",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||
}
|
||||
@@ -183,11 +186,13 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "allpayments",
|
||||
id: "header-accounting-allpayments",
|
||||
icon: <BankFilled />,
|
||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterpayments",
|
||||
id: "header-accounting-enterpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
@@ -203,6 +208,7 @@ function Header({
|
||||
if (ImEXPay.treatment === "on") {
|
||||
accountingChildren.push({
|
||||
key: "entercardpayments",
|
||||
id: "header-accounting-entercardpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
@@ -227,6 +233,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "timetickets",
|
||||
id: "header-accounting-timetickets",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
||||
}
|
||||
@@ -235,6 +242,7 @@ function Header({
|
||||
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
||||
accountingChildren.push({
|
||||
key: "ttapprovals",
|
||||
id: "header-accounting-ttapprovals",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||
});
|
||||
@@ -244,6 +252,7 @@ function Header({
|
||||
key: "entertimetickets",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
label: t("menus.header.entertimeticket"),
|
||||
id: "header-accounting-entertimetickets",
|
||||
onClick: () => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
@@ -264,6 +273,7 @@ function Header({
|
||||
const accountingExportChildren = [
|
||||
{
|
||||
key: "receivables",
|
||||
id: "header-accounting-receivables",
|
||||
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
|
||||
}
|
||||
];
|
||||
@@ -271,6 +281,7 @@ function Header({
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
|
||||
accountingExportChildren.push({
|
||||
key: "payables",
|
||||
id: "header-accounting-payables",
|
||||
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link>
|
||||
});
|
||||
}
|
||||
@@ -278,6 +289,7 @@ function Header({
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
|
||||
accountingExportChildren.push({
|
||||
key: "payments",
|
||||
id: "header-accounting-payments",
|
||||
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link>
|
||||
});
|
||||
}
|
||||
@@ -288,6 +300,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "exportlogs",
|
||||
id: "header-accounting-exportlogs",
|
||||
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link>
|
||||
}
|
||||
);
|
||||
@@ -301,6 +314,7 @@ function Header({
|
||||
) {
|
||||
accountingChildren.push({
|
||||
key: "accountingexport",
|
||||
id: "header-accounting-export",
|
||||
icon: <ExportOutlined />,
|
||||
label: t("menus.header.export"),
|
||||
children: accountingExportChildren
|
||||
@@ -604,14 +618,22 @@ function Header({
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// key: 'rescue',
|
||||
// icon: <Icon component={CarFilled}/>,
|
||||
// label: t("menus.header.rescueme"),
|
||||
// onClick: () => {
|
||||
// window.open("https://imexrescue.com/", "_blank");
|
||||
// }
|
||||
// },
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: false,
|
||||
promanager: false
|
||||
})
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
icon: <Icon component={CarFilled} />,
|
||||
label: t("menus.header.rescueme"),
|
||||
onClick: () => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Form, Input, notification, Switch } from "antd";
|
||||
import dayjs from "../../../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,6 +13,7 @@ import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../../../utils/day";
|
||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||
|
||||
@@ -275,7 +275,19 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item name="actual_delivery" label={t("jobs.fields.actual_delivery")} disabled={readOnly}>
|
||||
<Form.Item
|
||||
name="actual_delivery"
|
||||
label={t("jobs.fields.actual_delivery")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.deliverchecklist.actual_delivery
|
||||
? bodyshop.deliverchecklist.actual_delivery
|
||||
: false
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
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 { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -19,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
|
||||
|
||||
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 },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { LockOutlined } from "@ant-design/icons";
|
||||
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -12,9 +12,8 @@ import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
|
||||
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
|
||||
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
|
||||
import "./job-close-ro-guard.styles.scss";
|
||||
import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
|
||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CardTemplate from "./job-detail-cards.template.component";
|
||||
import Car from "../job-damage-visual/job-damage-visual.component";
|
||||
import CardTemplate from "./job-detail-cards.template.component";
|
||||
|
||||
export default function JobDetailCardsDamageComponent({ loading, data }) {
|
||||
const { t } = useTranslation();
|
||||
const { area_of_damage } = data;
|
||||
return (
|
||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
||||
{area_of_damage ? <Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} /> : t("jobs.errors.nodamage")}
|
||||
{area_of_damage ? (
|
||||
<Car
|
||||
dmg1={area_of_damage.impact1 && area_of_damage.impact1.padStart(2, "0")}
|
||||
dmg2={area_of_damage.impact2 && area_of_damage.impact2.padStart(2, "0")}
|
||||
/>
|
||||
) : (
|
||||
t("jobs.errors.nodamage")
|
||||
)}
|
||||
</CardTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,16 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
|
||||
const { t } = useTranslation();
|
||||
const { joblines_status } = data;
|
||||
|
||||
const filteredJobLines = data.joblines.filter(
|
||||
(j) =>
|
||||
j.part_type !== null &&
|
||||
j.part_type !== "PAE" &&
|
||||
j.part_type !== "PAS" &&
|
||||
j.part_type !== "PASL" &&
|
||||
j.part_qty !== 0 &&
|
||||
j.act_price !== 0
|
||||
);
|
||||
//TODO: Correct jobline_statuses view by including the part_qty !== 0 and act_price !== 0
|
||||
const columns = [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
@@ -95,7 +105,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
|
||||
<div>
|
||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
||||
<PartsStatusPie joblines_status={joblines_status} />
|
||||
<Table key="id" columns={columns} dataSource={data ? data.joblines : []} />
|
||||
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
|
||||
</CardTemplate>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,26 +2,30 @@ import { useQuery } from "@apollo/client";
|
||||
import { Col, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
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 { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
|
||||
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
|
||||
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
|
||||
const { t } = useTranslation();
|
||||
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
|
||||
fetchPolicy: "network-only",
|
||||
@@ -46,9 +50,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
{!technician ? (
|
||||
<>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
`${line.parts_order.order_number}`
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||
@@ -83,70 +93,81 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
|
||||
</Col>
|
||||
<Col span={8}>{line.parts_dispatch.number}</Col>
|
||||
<Col span={8}>
|
||||
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
{line.accepted_at ? (
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: {
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_orders.labels.notyetordered")
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_dispatch.labels.notyetdispatched")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<FeatureWrapper featureName="bills" noauth={() => null}>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<BillDetailEditcontainer />
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
{!technician ? (
|
||||
<>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
`${line.bill.invoice_number}`
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</FeatureWrapper>
|
||||
<Col md={24} lg={24}>
|
||||
<TaskListContainer
|
||||
parentJobId={jobid}
|
||||
|
||||
@@ -3,13 +3,21 @@ import { Button, Form, notification, Popover, Tooltip } from "antd";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_LINE_PPC } from "../../graphql/jobs-lines.queries";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.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 [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,
|
||||
rome: (
|
||||
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
|
||||
@@ -95,3 +103,4 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
</JobLineConvertToLabor>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesPartPriceChange);
|
||||
|
||||
@@ -31,18 +31,20 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import _ from "lodash";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import JobCreateIOU from "../job-create-iou/job-create-iou.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 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 PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
|
||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -53,6 +55,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setPartsReceiveContext: (context) => dispatch(setModalContext({ context: context, modal: "partsReceive" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
@@ -62,6 +65,7 @@ export function JobLinesComponent({
|
||||
jobRO,
|
||||
technician,
|
||||
setPartsOrderContext,
|
||||
setPartsReceiveContext,
|
||||
loading,
|
||||
refetch,
|
||||
jobLines,
|
||||
@@ -70,7 +74,11 @@ export function JobLinesComponent({
|
||||
setJobLineEditContext,
|
||||
form,
|
||||
setBillEnterContext,
|
||||
setTaskUpsertContext
|
||||
setTaskUpsertContext,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||
const {
|
||||
@@ -197,7 +205,6 @@ export function JobLinesComponent({
|
||||
onFilter: (value, record) => value.includes(record.part_type),
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
@@ -212,7 +219,6 @@ export function JobLinesComponent({
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
|
||||
// {
|
||||
// title: t('joblines.fields.tax_part'),
|
||||
// dataIndex: 'tax_part',
|
||||
@@ -321,7 +327,7 @@ export function JobLinesComponent({
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -336,7 +342,6 @@ export function JobLinesComponent({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
@@ -350,7 +355,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -436,6 +441,15 @@ export function JobLinesComponent({
|
||||
return (
|
||||
<div>
|
||||
<PartsOrderModalContainer />
|
||||
{!technician && (
|
||||
<PartsOrderDrawer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleOnRowClick={handlePartsOrderOnRowClick}
|
||||
setPartsReceiveContext={setPartsReceiveContext}
|
||||
setTaskUpsertContext={setTaskUpsertContext}
|
||||
/>
|
||||
)}
|
||||
<PageHeader
|
||||
title={t("jobs.labels.estimatelines")}
|
||||
extra={
|
||||
@@ -552,7 +566,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
{bodyshop.region_config.toLowerCase().startsWith("us") && <JobSendPartPriceChangeComponent job={job} />}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
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 jobLines = useMemo(() => {
|
||||
@@ -22,7 +32,19 @@ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
||||
}, [joblines, searchText]);
|
||||
|
||||
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 { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
technician: selectTechnician
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
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 [loading, setLoading] = useState(false);
|
||||
@@ -165,7 +166,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{jobline.act_price !== 0 && (
|
||||
{jobline.act_price !== 0 && !technician && (
|
||||
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
|
||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||
<Button
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobSendPartPriceChangeComponent({ job }) {
|
||||
export default function JobSendPartPriceChangeComponent({ job, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleClick = async () => {
|
||||
@@ -24,7 +24,7 @@ export default function JobSendPartPriceChangeComponent({ job }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} loading={loading}>
|
||||
<Button onClick={handleClick} loading={loading} disabled={disabled}>
|
||||
{t("jobs.actions.sendpartspricechange")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ import Dinero from "dinero.js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function JobTotalsTableLabor({ job }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -56,16 +56,49 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||
render: (text, record) => record.hours.toFixed(1)
|
||||
},
|
||||
{
|
||||
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()
|
||||
}
|
||||
...InstanceRenderManager({
|
||||
imex: [
|
||||
{
|
||||
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()
|
||||
}
|
||||
],
|
||||
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) => {
|
||||
@@ -91,6 +124,16 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<Table.Summary.Cell>
|
||||
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
|
||||
</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
@@ -122,7 +165,29 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
|
||||
</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.Cell>
|
||||
@@ -151,7 +216,29 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
|
||||
</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.Cell>
|
||||
@@ -159,6 +246,16 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
</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">
|
||||
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Space, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DELETE_DELIVERY_CHECKLIST, DELETE_INTAKE_CHECKLIST } from "../../graphql/jobs.queries";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function JobAdminDeleteIntake({ job }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -47,16 +48,22 @@ export default function JobAdminDeleteIntake({ job }) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
const InstanceRender = InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: "USE_IMEX",
|
||||
promanager: false
|
||||
});
|
||||
|
||||
return InstanceRender ? (
|
||||
<>
|
||||
<Space wrap>
|
||||
<Button loading={loading} onClick={handleDelete} disabled={!job.intakechecklist}>
|
||||
{t("jobs.labels.deleteintake")}
|
||||
</Button>
|
||||
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverychecklist}>
|
||||
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverchecklist}>
|
||||
{t("jobs.labels.deletedelivery")}
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Col, Row, notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -24,6 +23,8 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||
@@ -32,7 +33,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -580,12 +580,13 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
promanager: () => {
|
||||
rome: () => {
|
||||
if (line.mod_lbr_ty === "LAET" || line.mod_lbr_ty === "LAUT") {
|
||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||
line.mod_lbr_ty = "LAR";
|
||||
}
|
||||
}
|
||||
},
|
||||
promanager: "USE_ROME"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -189,7 +189,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
</Col>
|
||||
<Col {...lossColDamage}>
|
||||
{job.area_of_damage ? (
|
||||
<Car dmg1={job.area_of_damage.impact1} dmg2={job.area_of_damage.impact2} />
|
||||
<Car
|
||||
dmg1={job.area_of_damage.impact1 && job.area_of_damage.impact1.padStart(2, "0")}
|
||||
dmg2={job.area_of_damage.impact2 && job.area_of_damage.impact2.padStart(2, "0")}
|
||||
/>
|
||||
) : (
|
||||
t("jobs.errors.nodamage")
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -8,27 +11,24 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import axios from "axios";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import dayjs from "../../utils/day";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -83,6 +83,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
modal: "timeTicketTask"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "taskUpsert"
|
||||
})
|
||||
),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text))
|
||||
@@ -104,7 +111,8 @@ export function JobsDetailHeaderActions({
|
||||
setEmailOptions,
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
setTimeTicketTaskContext
|
||||
setTimeTicketTaskContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -633,6 +641,7 @@ export function JobsDetailHeaderActions({
|
||||
const menuItems = [
|
||||
{
|
||||
key: "schedule",
|
||||
id: "job-actions-schedule",
|
||||
disabled: !jobInPreProduction || !job.converted || jobRO,
|
||||
label: t("jobs.actions.schedule"),
|
||||
onClick: () => {
|
||||
@@ -649,6 +658,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "cancelallappointments",
|
||||
id: "job-actions-cancelallappointments",
|
||||
onClick: () => {
|
||||
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
|
||||
return;
|
||||
@@ -662,6 +672,7 @@ export function JobsDetailHeaderActions({
|
||||
imex: [
|
||||
{
|
||||
key: "intake",
|
||||
id: "job-actions-intake",
|
||||
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
||||
label:
|
||||
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
||||
@@ -672,6 +683,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "deliver",
|
||||
id: "job-actions-deliver",
|
||||
disabled: !jobInProduction || jobRO,
|
||||
label: !jobInProduction ? (
|
||||
t("jobs.actions.deliver")
|
||||
@@ -681,6 +693,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "checklist",
|
||||
id: "job-actions-checklist",
|
||||
disabled: !job.converted,
|
||||
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
|
||||
}
|
||||
@@ -689,6 +702,7 @@ export function JobsDetailHeaderActions({
|
||||
promanager: [
|
||||
{
|
||||
key: "toggleproduction",
|
||||
id: "job-actions-toggleproduction",
|
||||
disabled: !job.converted || jobRO,
|
||||
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
|
||||
}
|
||||
@@ -702,6 +716,7 @@ export function JobsDetailHeaderActions({
|
||||
? [
|
||||
{
|
||||
key: "entertimetickets",
|
||||
id: "job-actions-entertimetickets",
|
||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
label: t("timetickets.actions.enter"),
|
||||
onClick: () => {
|
||||
@@ -725,6 +740,7 @@ export function JobsDetailHeaderActions({
|
||||
if (bodyshop.md_tasks_presets.enable_tasks) {
|
||||
menuItems.push({
|
||||
key: "claimtimetickettasks",
|
||||
id: "job-actions-claimtimetickettasks",
|
||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
onClick: () => {
|
||||
setTimeTicketTaskContext({
|
||||
@@ -738,6 +754,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "enterpayments",
|
||||
id: "job-actions-enterpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
@@ -753,22 +770,24 @@ export function JobsDetailHeaderActions({
|
||||
if (ImEXPay.treatment === "on") {
|
||||
menuItems.push({
|
||||
key: "entercardpayments",
|
||||
id: "job-actions-entercardpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
logImEXEvent("job_header_enter_card_payment");
|
||||
|
||||
setCardPaymentContext({
|
||||
actions: {},
|
||||
actions: { refetch },
|
||||
context: { jobid: job.id }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (HasFeatureAccess({ featureName: "courtesycars" })) {
|
||||
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
|
||||
menuItems.push({
|
||||
key: "cccontract",
|
||||
id: "job-actions-cccontract",
|
||||
disabled: jobRO || !job.converted,
|
||||
label: (
|
||||
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
|
||||
@@ -778,16 +797,29 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
key: "createtask",
|
||||
id: "job-actions-createtask",
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () =>
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id }
|
||||
})
|
||||
});
|
||||
|
||||
menuItems.push(
|
||||
job.inproduction
|
||||
? {
|
||||
key: "removefromproduction",
|
||||
id: "job-actions-removefromproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.removefromproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch, true)
|
||||
}
|
||||
: {
|
||||
key: "addtoproduction",
|
||||
id: "job-actions-addtoproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.addtoproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch)
|
||||
@@ -797,12 +829,14 @@ export function JobsDetailHeaderActions({
|
||||
menuItems.push(
|
||||
{
|
||||
key: "togglesuspend",
|
||||
id: "job-actions-togglesuspend",
|
||||
onClick: handleSuspend,
|
||||
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
|
||||
},
|
||||
{
|
||||
key: "toggleAlert",
|
||||
onClick: handleAlertToggle,
|
||||
id: "job-actions-togglealert",
|
||||
label:
|
||||
job.production_vars && job.production_vars.alert
|
||||
? t("production.labels.alertoff")
|
||||
@@ -814,6 +848,7 @@ export function JobsDetailHeaderActions({
|
||||
children: [
|
||||
{
|
||||
key: "duplicate",
|
||||
id: "job-actions-duplicate",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -829,6 +864,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "duplicatenolines",
|
||||
id: "job-actions-duplicatenolines",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -852,6 +888,7 @@ export function JobsDetailHeaderActions({
|
||||
? [
|
||||
{
|
||||
key: "postbills",
|
||||
id: "job-actions-postbills",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.postbills"),
|
||||
onClick: () => {
|
||||
@@ -870,6 +907,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
{
|
||||
key: "addtopartsqueue",
|
||||
id: "job-actions-addtopartsqueue",
|
||||
disabled: !job.converted || !jobInProduction || jobRO,
|
||||
label: t("jobs.actions.addtopartsqueue"),
|
||||
onClick: async () => {
|
||||
@@ -895,6 +933,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "closejob",
|
||||
id: "job-actions-closejob",
|
||||
disabled: !jobInPostProduction,
|
||||
label: !jobInPostProduction ? (
|
||||
t("menus.jobsactions.closejob")
|
||||
@@ -910,6 +949,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "admin",
|
||||
id: "job-actions-admin",
|
||||
label: (
|
||||
<Link
|
||||
to={{
|
||||
@@ -931,6 +971,7 @@ export function JobsDetailHeaderActions({
|
||||
) {
|
||||
menuItems.push({
|
||||
key: "exportcustdata",
|
||||
id: "job-actions-exportcustdata",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.exportcustdata"),
|
||||
onClick: handleExportCustData
|
||||
@@ -941,18 +982,21 @@ export function JobsDetailHeaderActions({
|
||||
const children = [
|
||||
{
|
||||
key: "email",
|
||||
id: "job-actions-email",
|
||||
disabled: !!!job.ownr_ea,
|
||||
label: t("general.labels.email"),
|
||||
onClick: handleCreateCsi
|
||||
},
|
||||
{
|
||||
key: "text",
|
||||
id: "job-actions-text",
|
||||
disabled: !!!job.ownr_ph1,
|
||||
label: t("general.labels.text"),
|
||||
onClick: handleCreateCsi
|
||||
},
|
||||
{
|
||||
key: "generate",
|
||||
id: "job-actions-generate",
|
||||
disabled: job.csiinvites && job.csiinvites.length > 0,
|
||||
label: t("jobs.actions.generatecsi"),
|
||||
onClick: handleCreateCsi
|
||||
@@ -986,6 +1030,7 @@ export function JobsDetailHeaderActions({
|
||||
}
|
||||
menuItems.push({
|
||||
key: "sendcsi",
|
||||
id: "job-actions-sendcsi",
|
||||
label: t("jobs.actions.sendcsi"),
|
||||
disabled: !job.converted,
|
||||
children
|
||||
@@ -994,6 +1039,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "jobcosting",
|
||||
id: "job-actions-jobcosting",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.labels.jobcosting"),
|
||||
onClick: () => {
|
||||
@@ -1011,6 +1057,7 @@ export function JobsDetailHeaderActions({
|
||||
if (job && !job.converted) {
|
||||
menuItems.push({
|
||||
key: "deletejob",
|
||||
id: "job-actions-deletejob",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
@@ -1027,6 +1074,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "manualevent",
|
||||
id: "job-actions-manualevent",
|
||||
onClick: (e) => {
|
||||
setVisibility(true);
|
||||
},
|
||||
@@ -1036,6 +1084,7 @@ export function JobsDetailHeaderActions({
|
||||
if (!jobRO && job.converted) {
|
||||
menuItems.push({
|
||||
key: "voidjob",
|
||||
id: "job-actions-voidjob",
|
||||
label: (
|
||||
<RbacWrapper action="jobs:void" noauth>
|
||||
<Popconfirm
|
||||
|
||||
@@ -1,55 +1,13 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
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";
|
||||
|
||||
export default function JobsDetailPliContainer({ job }) {
|
||||
const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
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) });
|
||||
}
|
||||
};
|
||||
export default function JobsDetailPliContainer({
|
||||
job,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
return (
|
||||
<JobsDetailPliComponent
|
||||
job={job}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Collapse, Form, Switch } from "antd";
|
||||
import { Collapse, Form, InputNumber, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -17,6 +17,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
<Collapse defaultActiveKey={expanded && "rates"}>
|
||||
<Collapse.Panel forceRender header={t("jobs.labels.cieca_pfl")} key="cieca_pfl">
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAB", "lbr_tx_in1"]}
|
||||
@@ -61,6 +82,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAD", "lbr_tx_in1"]}
|
||||
@@ -105,6 +147,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAE", "lbr_tx_in1"]}
|
||||
@@ -149,6 +212,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAF", "lbr_tx_in1"]}
|
||||
@@ -193,6 +277,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAG", "lbr_tx_in1"]}
|
||||
@@ -237,6 +342,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAM", "lbr_tx_in1"]}
|
||||
@@ -281,6 +407,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAR", "lbr_tx_in1"]}
|
||||
@@ -325,6 +472,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAS", "lbr_tx_in1"]}
|
||||
@@ -369,6 +537,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<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
|
||||
label={t("jobs.fields.cieca_pfl.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} />
|
||||
</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
|
||||
label={t("jobs.fields.cieca_pfl.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"]}>
|
||||
<Input disabled={jobRO} />
|
||||
</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
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["materials", "MAPA", "tax_ind"]}
|
||||
@@ -31,6 +33,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
>
|
||||
<Switch />
|
||||
</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
|
||||
label={t("jobs.fields.materials.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"]}>
|
||||
<Input disabled={jobRO} />
|
||||
</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
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["materials", "MASH", "tax_ind"]}
|
||||
@@ -82,6 +104,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
>
|
||||
<Switch />
|
||||
</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
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["materials", "MASH", "mat_tx_in1"]}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
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">
|
||||
<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">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</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>
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<>
|
||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</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">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<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">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -18,7 +18,6 @@ import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -315,34 +314,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
title={t("titles.bc.jobs-active")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{InstanceRenderManager({
|
||||
promanager: (
|
||||
<Button
|
||||
onClick={() =>
|
||||
setJoyRideSteps([
|
||||
{
|
||||
target: "#active-jobs-list",
|
||||
content: "This is where you will see all work coming in and currently here."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs",
|
||||
spotlightClicks: true,
|
||||
disableOverlayClose: true,
|
||||
content:
|
||||
"The jobs menu lets you access additional pages to see more information. You can import new jobs, search all jobs, or manage your current production."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs-available",
|
||||
content: "You can find jobs to import here."
|
||||
}
|
||||
])
|
||||
}
|
||||
>
|
||||
Start Walk Through
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -75,7 +76,18 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
})),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.actual_completion"),
|
||||
dataIndex: "actual_completion",
|
||||
key: "actual_completion",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_completion" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPartsReceiveContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsReceive"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function PartsOrderListTableDrawerComponent({
|
||||
setBillEnterContext,
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "75%",
|
||||
xl: "75%",
|
||||
xxl: "65%"
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
const Templates = TemplateList("partsorder", { job });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill }
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
setPartsReceiveContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
partsorderlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
id: pol.id,
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
act_price: pol.act_price,
|
||||
oem_partno: pol.oem_partno
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receive")}
|
||||
</Button>
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
partsorderid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("parts_orders.labels.confirmdelete")}
|
||||
disabled={jobRO}
|
||||
onConfirm={async () => {
|
||||
//Delete the parts return.!
|
||||
|
||||
await deletePartsOrder({
|
||||
variables: { partsOrderId: record.id },
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
parts_orders(existingPartsOrders, { readField }) {
|
||||
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button disabled={jobRO}>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
|
||||
<Button
|
||||
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
job: job,
|
||||
bill: {
|
||||
vendorid: record.vendor.id,
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
actual_price: pol.act_price,
|
||||
|
||||
cost_center: pol.jobline?.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? pol.jobline.part_type !== "PAE"
|
||||
? pol.jobline.part_type
|
||||
: null
|
||||
: responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
|
||||
: null
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receivebill")}
|
||||
</Button>
|
||||
</FeatureWrapperComponent>
|
||||
<PrintWrapper
|
||||
templateObject={{
|
||||
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
|
||||
variables: { id: record.id }
|
||||
}}
|
||||
messageObject={{
|
||||
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
|
||||
to: record.vendor.email
|
||||
}}
|
||||
id={job.id}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderListTableDrawerComponent);
|
||||
@@ -1,32 +1,23 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import queryString from "query-string";
|
||||
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd";
|
||||
import React, { 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 { 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 { alphaSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import DataLabel from "../data-label/data-label.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 { alphaSort } from "../../utils/sorters";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -61,19 +52,6 @@ export function PartsOrderListTableComponent({
|
||||
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 });
|
||||
|
||||
@@ -81,10 +59,8 @@ export function PartsOrderListTableComponent({
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
@@ -93,7 +69,11 @@ export function PartsOrderListTableComponent({
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
@@ -175,7 +155,7 @@ export function PartsOrderListTableComponent({
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
@@ -270,147 +250,6 @@ export function PartsOrderListTableComponent({
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} extra={recordActions(record)} />
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredPartsOrders = parts_orders
|
||||
? searchText === ""
|
||||
? parts_orders
|
||||
@@ -441,15 +280,13 @@ export function PartsOrderListTableComponent({
|
||||
}
|
||||
>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
<PartsOrderDrawer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleOnRowClick={handleOnRowClick}
|
||||
setPartsReceiveContext={setPartsReceiveContext}
|
||||
setTaskUpsertContext={setTaskUpsertContext}
|
||||
/>
|
||||
<Table
|
||||
loading={billsQuery.loading}
|
||||
scroll={{
|
||||
|
||||
@@ -119,8 +119,14 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Descriptions title={t("job_payments.titles.descriptions")} contentStyle={{ fontWeight: "600" }} column={4}>
|
||||
<Descriptions.Item label={t("job_payments.titles.payer")}>{record.payer}</Descriptions.Item>
|
||||
<Descriptions
|
||||
title={t("job_payments.titles.descriptions")}
|
||||
contentStyle={{ fontWeight: "600" }}
|
||||
column={4}
|
||||
>
|
||||
<Descriptions.Item label={t("job_payments.titles.hint")}>
|
||||
{payment_response?.response?.methodhint}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.payername")}>
|
||||
{payment_response?.response?.nameOnCard ?? ""}
|
||||
</Descriptions.Item>
|
||||
@@ -132,7 +138,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{record.transactionid}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
|
||||
{payment_response?.response?.paymentreferenceid ?? ""}
|
||||
{payment_response?.ext_paymentid ?? ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>{record.type}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>
|
||||
|
||||
@@ -17,7 +17,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
|
||||
|
||||
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
{loading && <Spin />}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.imex-kanban-card {
|
||||
padding: 0px !important;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
padding: 0rem 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CalendarOutlined,
|
||||
DownloadOutlined,
|
||||
EyeFilled,
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import "./production-board-card.styles.scss";
|
||||
import dayjs from "../../utils/day";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
|
||||
|
||||
let color = { r: 255, g: 255, b: 255 };
|
||||
|
||||
if (bucket && bucket.color) {
|
||||
color = bucket.color;
|
||||
|
||||
if (bucket.color.rgb) {
|
||||
color = bucket.color.rgb;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
function getContrastYIQ(bgColor) {
|
||||
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
|
||||
|
||||
return yiq >= 128 ? "black" : "white";
|
||||
}
|
||||
|
||||
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let employee_body, employee_prep, employee_refinish, employee_csr;
|
||||
if (card.employee_body) {
|
||||
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
|
||||
}
|
||||
if (card.employee_prep) {
|
||||
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
|
||||
}
|
||||
if (card.employee_refinish) {
|
||||
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
|
||||
}
|
||||
if (card.employee_csr) {
|
||||
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
||||
}
|
||||
// if (card.employee_csr) {
|
||||
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
||||
// }
|
||||
|
||||
const pastDueAlert =
|
||||
!!card.scheduled_completion &&
|
||||
((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") ||
|
||||
(dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon"));
|
||||
|
||||
const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="react-kanban-card imex-kanban-card"
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor:
|
||||
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<ProductionAlert record={card} key="alert" />
|
||||
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{card.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<span style={{ fontWeight: "bolder" }}>
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{card.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<Row>
|
||||
{cardSettings && cardSettings.ownr_nm && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.compact ? (
|
||||
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
|
||||
) : (
|
||||
<div className="ellipses">
|
||||
<OwnerNameDisplay ownerObject={card} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<div className="ellipses">{`${card.v_model_yr || ""} ${
|
||||
card.v_make_desc || ""
|
||||
} ${card.v_model_desc || ""}`}</div>
|
||||
</Col>
|
||||
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.ins_co_nm || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.clm_no && card.clm_no && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.clm_no || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{cardSettings && cardSettings.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
|
||||
} ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`P: ${
|
||||
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
|
||||
}`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
employee_refinish
|
||||
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
|
||||
: ""
|
||||
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
|
||||
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
|
||||
}`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{/* {cardSettings && cardSettings.laborhrs && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)} */}
|
||||
{cardSettings && cardSettings.actual_in && card.actual_in && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.ats && card.alt_transport && (
|
||||
<Col span={12}>
|
||||
<div>{card.alt_transport || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.sublets && (
|
||||
<Col span={12}>
|
||||
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.production_note && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.partsstatus && (
|
||||
<Col span={24}>
|
||||
<JobPartsQueueCount parts={card.joblines_status} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CalendarOutlined,
|
||||
DownloadOutlined,
|
||||
EyeFilled,
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||
return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
|
||||
};
|
||||
|
||||
const getContrastYIQ = (bgColor) =>
|
||||
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
|
||||
|
||||
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
|
||||
|
||||
const EllipsesToolTip = React.memo(({ title, children, kiosk }) => {
|
||||
if (kiosk || !title) {
|
||||
return <div className="ellipses no-select">{children}</div>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<div className="ellipses">{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ownr_nm && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip
|
||||
title={metadata.ownr_ln || metadata.ownr_co_nm ? <OwnerNameDisplay ownerObject={metadata} /> : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.ownr_ln || metadata.ownr_co_nm ? (
|
||||
cardSettings.compact ? (
|
||||
`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`
|
||||
) : (
|
||||
<OwnerNameDisplay ownerObject={metadata} />
|
||||
)
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ModelInfoToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.model_info && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc
|
||||
? `${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc ? (
|
||||
`${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const InsuranceCompanyToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ins_co_nm && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.ins_co_nm || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.ins_co_nm ? metadata.ins_co_nm : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ClaimNumberToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.clm_no && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.clm_no || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.clm_no ? metadata.clm_no : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const EmployeeAssignmentsToolTip = ({
|
||||
metadata,
|
||||
cardSettings,
|
||||
employee_body,
|
||||
employee_prep,
|
||||
employee_refinish,
|
||||
employee_csr
|
||||
}) =>
|
||||
cardSettings?.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs
|
||||
? `B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs ? (
|
||||
`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_prep
|
||||
? `P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_prep ? (
|
||||
`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||
? `R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs ? (
|
||||
`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_csr ? `C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}` : null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_csr ? (
|
||||
`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ActualInToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.actual_in && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.actual_in || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.actual_in ? (
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const EstimatorToolTip = ({ metadata, cardSettings }) => {
|
||||
return (
|
||||
cardSettings?.estimator && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={metadata.est_ct_fn && metadata.est_ct_ln ? `${metadata.est_ct_fn} ${metadata.est_ct_ln}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.est_ct_fn && metadata.est_ct_ln ? (
|
||||
<span>E: {`${metadata.est_ct_fn} ${metadata.est_ct_ln}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
|
||||
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
|
||||
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
|
||||
|
||||
return (
|
||||
cardSettings?.subtotal && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{!!amount ? (
|
||||
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ScheduledCompletionToolTip = ({ metadata, cardSettings, pastDueAlert }) =>
|
||||
cardSettings?.scheduled_completion && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.scheduled_completion || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.scheduled_completion ? (
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const AltTransportToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ats && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip title={metadata.alt_transport || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.alt_transport ? metadata.alt_transport : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const SubletsComponent = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.sublets && (
|
||||
<Col span={12}>
|
||||
{metadata.subletLines ? (
|
||||
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ProductionNoteComponent = ({ metadata, cardSettings, card }) =>
|
||||
cardSettings?.production_note && (
|
||||
<Col span={24} style={{ margin: "2px 0" }}>
|
||||
<ProductionListColumnProductionNote
|
||||
record={{
|
||||
production_vars: metadata?.production_vars,
|
||||
id: card?.id,
|
||||
refetch: card?.refetch
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const PartsStatusComponent = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.partsstatus && (
|
||||
<Col span={24} style={{ textAlign: "center" }}>
|
||||
{metadata.joblines_status ? <JobPartsQueueCount parts={metadata.joblines_status} /> : <span> </span>}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const TasksToolTip = ({ metadata, cardSettings, t }) =>
|
||||
cardSettings?.tasks && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip
|
||||
title={`${t("production.labels.tasks")}: ${metadata.tasks_aggregate?.aggregate?.count || 0}`}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.tasks_aggregate?.aggregate?.count ? (
|
||||
`T: ${metadata.tasks_aggregate.aggregate.count}`
|
||||
) : (
|
||||
<span>T: 0</span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
|
||||
const { t } = useTranslation();
|
||||
const { metadata } = card;
|
||||
|
||||
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
||||
|
||||
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
||||
return {
|
||||
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
|
||||
employee_prep: metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep),
|
||||
employee_refinish: metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish),
|
||||
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
|
||||
};
|
||||
}, [metadata, employees]);
|
||||
|
||||
const pastDueAlert = useMemo(() => {
|
||||
if (!metadata?.scheduled_completion) return null;
|
||||
const completionDate = dayjs(metadata.scheduled_completion);
|
||||
if (dayjs().isSameOrAfter(completionDate, "day")) return "production-completion-past";
|
||||
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
|
||||
return null;
|
||||
}, [metadata?.scheduled_completion]);
|
||||
|
||||
const totalHrs = useMemo(() => {
|
||||
return metadata?.labhrs && metadata?.larhrs
|
||||
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0;
|
||||
}, [metadata?.labhrs, metadata?.larhrs]);
|
||||
|
||||
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
|
||||
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
|
||||
|
||||
const isBodyEmpty = useMemo(() => {
|
||||
return !(
|
||||
cardSettings?.ownr_nm ||
|
||||
cardSettings?.model_info ||
|
||||
cardSettings?.ins_co_nm ||
|
||||
cardSettings?.clm_no ||
|
||||
cardSettings?.employeeassignments ||
|
||||
cardSettings?.actual_in ||
|
||||
cardSettings?.scheduled_completion ||
|
||||
cardSettings?.ats ||
|
||||
cardSettings?.sublets ||
|
||||
cardSettings?.production_note ||
|
||||
cardSettings?.partsstatus ||
|
||||
cardSettings?.estimator ||
|
||||
cardSettings?.subtotal ||
|
||||
cardSettings?.tasks
|
||||
);
|
||||
}, [cardSettings]);
|
||||
|
||||
const headerContent = (
|
||||
<div className="header-content-container">
|
||||
<div className="inner-container">
|
||||
<ProductionAlert id={card.id} productionVars={metadata?.production_vars} refetch={card?.refetch} key="alert" />
|
||||
{metadata?.suspended && <PauseCircleOutlined className="circle-outline" key="suspended" />}
|
||||
{metadata?.iouparent && (
|
||||
<EllipsesToolTip
|
||||
title={t("jobs.labels.iou")}
|
||||
key="iouparent"
|
||||
className="iouparent"
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
<BranchesOutlined className="branches-outlined" />
|
||||
</EllipsesToolTip>
|
||||
)}
|
||||
</div>
|
||||
<span className="tech-container">
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{metadata?.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
{isBodyEmpty && (
|
||||
<div className="body-empty-container">
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const bodyContent = (
|
||||
<Row>
|
||||
<OwnerNameToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ModelInfoToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<InsuranceCompanyToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ClaimNumberToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<EmployeeAssignmentsToolTip
|
||||
metadata={metadata}
|
||||
cardSettings={cardSettings}
|
||||
employee_body={employee_body}
|
||||
employee_prep={employee_prep}
|
||||
employee_refinish={employee_refinish}
|
||||
employee_csr={employee_csr}
|
||||
/>
|
||||
<EstimatorToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<TasksToolTip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||
<SubtotalTooltip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||
<ActualInToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ScheduledCompletionToolTip metadata={metadata} cardSettings={cardSettings} pastDueAlert={pastDueAlert} />
|
||||
<AltTransportToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
|
||||
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
||||
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings?.cardcolor && contrastYIQ
|
||||
}}
|
||||
title={!isBodyEmpty ? headerContent : null}
|
||||
extra={
|
||||
!isBodyEmpty && (
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
>
|
||||
{isBodyEmpty ? headerContent : bodyContent}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, notification, Popover, Row, Switch } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
|
||||
export default function ProductionBoardKanbanCardSettings({ associationSettings }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
|
||||
}, [form, associationSettings, open]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: associationSettings && associationSettings.id,
|
||||
ks: values
|
||||
}
|
||||
});
|
||||
if (result.errors) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("production.errors.settings", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<div>
|
||||
<Card>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t("production.labels.compact")} name="compact" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ownr_nm")} name="ownr_nm">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.clm_no")} name="clm_no">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ins_co_nm")} name="ins_co_nm">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.laborhrs")}
|
||||
name="laborhrs"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item> */}
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.employeeassignments")}
|
||||
name="employeeassignments"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.actual_in")} name="actual_in">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.cardcolor")} name="cardcolor">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.scheduled_completion")}
|
||||
name="scheduled_completion"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ats")} name="ats">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.production_note")} name="production_note">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
valuePropName='checked' label={t("production.labels.alert")} name="alert">
|
||||
<Switch/>
|
||||
</Form.Item> */}
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.sublets")} name="sublets">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.partsstatus")} name="partsstatus">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.stickyheader")} name="stickyheader">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(true)}>
|
||||
{t("production.labels.cardsettings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,265 +1,237 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import Board, { moveCard } from "@asseinfo/react-kanban";
|
||||
import { Button, Grid, notification, Space, Statistic } from "antd";
|
||||
import Board from "./trello-board/index";
|
||||
import { Button, notification, Skeleton, Space } from "antd";
|
||||
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 { connect } from "react-redux";
|
||||
import { Sticky, StickyContainer } from "react-sticky";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import styled from "styled-components";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.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 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-color-legend.component.jsx";
|
||||
import "./production-board-kanban.styles.scss";
|
||||
import { createBoardData } from "./production-board-kanban.utils.js";
|
||||
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanComponent({
|
||||
data,
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
insertAuditTrail,
|
||||
associationSettings
|
||||
}) {
|
||||
const [boardLanes, setBoardLanes] = useState({
|
||||
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
|
||||
});
|
||||
|
||||
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
|
||||
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
|
||||
const [filter, setFilter] = useState({ search: "", employeeId: null });
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const [orientation, setOrientation] = useState("vertical");
|
||||
|
||||
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 handleDragEnd = async (card, source, destination) => {
|
||||
logImEXEvent("kanban_drag_end");
|
||||
useEffect(() => {
|
||||
if (associationSettings) {
|
||||
setLoading(true);
|
||||
setOrientation(associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal");
|
||||
setLoading(false);
|
||||
}
|
||||
}, [associationSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMoving(true);
|
||||
setBoardLanes(moveCard(boardLanes, source, destination));
|
||||
const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
|
||||
const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
|
||||
const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
|
||||
|
||||
const movedCardWillBeFirst = destination.toPosition === 0;
|
||||
|
||||
const 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"
|
||||
const newBoardData = createBoardData({
|
||||
statuses,
|
||||
data,
|
||||
filter,
|
||||
cardSettings: associationSettings?.kanban_settings
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: JSON.stringify(update.errors)
|
||||
})
|
||||
});
|
||||
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
|
||||
...lane,
|
||||
title: `${lane.title} (${lane.cards.length})`
|
||||
}));
|
||||
|
||||
setBoardLanes((prevBoardLanes) => {
|
||||
const deepClonedData = cloneDeep(newBoardData);
|
||||
if (!isEqual(prevBoardLanes, deepClonedData)) {
|
||||
return deepClonedData;
|
||||
}
|
||||
return prevBoardLanes;
|
||||
});
|
||||
setIsMoving(false);
|
||||
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
|
||||
|
||||
const getCardByID = useCallback((data, cardId) => {
|
||||
for (const lane of data.lanes) {
|
||||
for (const card of lane.cards) {
|
||||
if (card.id === cardId) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const totalHrs = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
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 onDragEnd = useCallback(
|
||||
async ({ type, source, destination, draggableId }) => {
|
||||
logImEXEvent("kanban_drag_end");
|
||||
|
||||
const standardSizes = {
|
||||
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"
|
||||
};
|
||||
if (!type || type !== "lane" || !source || !destination || isMoving) return;
|
||||
|
||||
const width = selectedBreakpoint
|
||||
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
|
||||
? compactSizes[selectedBreakpoint[0]]
|
||||
: standardSizes[selectedBreakpoint[0]]
|
||||
: "250";
|
||||
setIsMoving(true);
|
||||
|
||||
const stickyHeader = {
|
||||
renderColumnHeader: ({ title }) => (
|
||||
<Sticky>
|
||||
{({
|
||||
style,
|
||||
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
|
||||
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
|
||||
|
||||
// the following are also available but unused in this example
|
||||
isSticky,
|
||||
wasSticky,
|
||||
distanceFromTop,
|
||||
distanceFromBottom,
|
||||
calculatedHeight
|
||||
}) => (
|
||||
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
</Sticky>
|
||||
)
|
||||
};
|
||||
if (!targetLane || !sourceLane) {
|
||||
setIsMoving(false);
|
||||
console.error("Invalid source or destination lane");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardSettings =
|
||||
associationSettings &&
|
||||
associationSettings.kanban_settings &&
|
||||
Object.keys(associationSettings.kanban_settings).length > 0
|
||||
? associationSettings.kanban_settings
|
||||
: {
|
||||
ats: true,
|
||||
clm_no: true,
|
||||
compact: false,
|
||||
ownr_nm: true,
|
||||
sublets: true,
|
||||
ins_co_nm: true,
|
||||
production_note: true,
|
||||
employeeassignments: true,
|
||||
scheduled_completion: true,
|
||||
stickyheader: false,
|
||||
cardcolor: false
|
||||
};
|
||||
const sameColumnTransfer = source.droppableId === destination.droppableId;
|
||||
const sourceCard = getCardByID(boardLanes, draggableId);
|
||||
|
||||
const movedCardWillBeFirst = destination.index === 0;
|
||||
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
|
||||
|
||||
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
|
||||
const oldChildCard = sourceLane.cards[source.index + 1];
|
||||
|
||||
const newChildCard = movedCardWillBeLast
|
||||
? null
|
||||
: targetLane.cards[
|
||||
sameColumnTransfer
|
||||
? source.index < destination.index
|
||||
? destination.index + 1
|
||||
: destination.index
|
||||
: 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(() => {
|
||||
const kanbanSettings = associationSettings?.kanban_settings;
|
||||
return mergeWithDefaults(kanbanSettings);
|
||||
}, [associationSettings]);
|
||||
|
||||
const handleSettingsChange = useCallback((newSettings) => {
|
||||
setLoading(true);
|
||||
setOrientation(newSettings.orientation ? "vertical" : "horizontal");
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton active />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container width={width}>
|
||||
<div>
|
||||
<IndefiniteLoading loading={isMoving} />
|
||||
|
||||
<PageHeader
|
||||
title={
|
||||
<Space>
|
||||
<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>
|
||||
}
|
||||
title={cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
style={{ paddingInline: 0, paddingBlock: 0 }}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch && refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
||||
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
|
||||
<ProductionBoardKanbanSettings
|
||||
parentLoading={setLoading}
|
||||
associationSettings={associationSettings}
|
||||
onSettingsChange={handleSettingsChange}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
|
||||
<NoteUpsertModal />
|
||||
<ProductionListDetailComponent jobs={data} />
|
||||
<StickyContainer>
|
||||
<Board
|
||||
style={{ height: "100%" }}
|
||||
children={boardLanes}
|
||||
disableCardDrag={isMoving}
|
||||
{...(cardSettings.stickyheader && stickyHeader)}
|
||||
renderCard={(card) => ProductionBoardCard(technician, card, bodyshop, cardSettings)}
|
||||
onCardDragEnd={handleDragEnd}
|
||||
/>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
<Board
|
||||
queryData={data}
|
||||
data={boardLanes}
|
||||
onDragEnd={onDragEnd}
|
||||
orientation={orientation}
|
||||
cardSettings={cardSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 _ from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
@@ -18,76 +12,53 @@ const mapStateToProps = createStructuredSelector({
|
||||
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, {
|
||||
pollInterval: 3600000,
|
||||
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(() => {
|
||||
if (!(data && data.jobs)) return;
|
||||
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 { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
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 (
|
||||
<ProductionBoardKanbanComponent
|
||||
loading={loading || associationSettingsLoading}
|
||||
data={data ? data.jobs : []}
|
||||
refetch={refetch}
|
||||
associationSettings={
|
||||
associationSettings && associationSettings.associations[0] ? associationSettings.associations[0] : null
|
||||
}
|
||||
associationSettings={filteredAssociationSettings}
|
||||
bodyshop={bodyshop}
|
||||
statuses={combinedStatuses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ProductionBoardKanbanContainer);
|
||||
export default connect(mapStateToProps)(ProductionBoardKanbanContainer);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
|
||||
|
||||
export const StatisticType = {
|
||||
HOURS: "hours",
|
||||
AMOUNT: "amount",
|
||||
JOBS: "jobs",
|
||||
TASKS: "tasks"
|
||||
};
|
||||
|
||||
const mergeStatistics = (items, values) => {
|
||||
const valuesMap = values.reduce((acc, value) => {
|
||||
acc[value.id] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
value: valuesMap[item.id]?.value,
|
||||
type: valuesMap[item.id]?.type
|
||||
}));
|
||||
};
|
||||
|
||||
const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const calculateTotal = (items, key, subKey) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
|
||||
};
|
||||
|
||||
const calculateTotalAmount = (items, key) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotal = (lanes, key, subKey) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.aggregate?.sum?.[subKey] || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotalAmount = (lanes, key) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const formatValue = (value, type) => {
|
||||
if (type === StatisticType.JOBS) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
if (type === StatisticType.HOURS) {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const totalHrs = useMemo(() => {
|
||||
if (!cardSettings.totalHrs) return null;
|
||||
const total = calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalHrs]);
|
||||
|
||||
const totalLAB = useMemo(() => {
|
||||
if (!cardSettings.totalLAB) return null;
|
||||
const total = calculateTotal(data, "labhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalLAB]);
|
||||
|
||||
const totalLAR = useMemo(() => {
|
||||
if (!cardSettings.totalLAR) return null;
|
||||
const total = calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalLAR]);
|
||||
|
||||
const jobsInProduction = useMemo(
|
||||
() => (cardSettings.jobsInProduction ? data.length : null),
|
||||
[data, cardSettings.jobsInProduction]
|
||||
);
|
||||
|
||||
const totalAmountInProduction = useMemo(() => {
|
||||
if (!cardSettings.totalAmountInProduction) return null;
|
||||
const total = calculateTotalAmount(data, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalAmountInProduction]);
|
||||
|
||||
const totalHrsOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
|
||||
const total =
|
||||
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
|
||||
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalHrsOnBoard]);
|
||||
|
||||
const totalLABOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalLABOnBoard) return null;
|
||||
const total = calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalLABOnBoard]);
|
||||
|
||||
const totalLAROnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalLAROnBoard) return null;
|
||||
const total = calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalLAROnBoard]);
|
||||
|
||||
const jobsOnBoard = useMemo(
|
||||
() =>
|
||||
reducerData && cardSettings.jobsOnBoard
|
||||
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
|
||||
: null,
|
||||
[reducerData, cardSettings.jobsOnBoard]
|
||||
);
|
||||
|
||||
const totalAmountOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
|
||||
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalAmountOnBoard]);
|
||||
|
||||
const tasksInProduction = useMemo(() => {
|
||||
if (!data || !cardSettings.tasksInProduction) return null;
|
||||
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
|
||||
}, [data, cardSettings.tasksInProduction]);
|
||||
|
||||
const tasksOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.tasksOnBoard) return null;
|
||||
return reducerData.lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
}, [reducerData, cardSettings.tasksOnBoard]);
|
||||
|
||||
const statistics = useMemo(
|
||||
() =>
|
||||
mergeStatistics(statisticsItems, [
|
||||
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
|
||||
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
|
||||
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
|
||||
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
|
||||
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
|
||||
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
|
||||
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
|
||||
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
|
||||
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
|
||||
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
|
||||
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
|
||||
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
|
||||
]),
|
||||
[
|
||||
totalHrs,
|
||||
totalAmountInProduction,
|
||||
totalLAB,
|
||||
totalLAR,
|
||||
jobsInProduction,
|
||||
totalHrsOnBoard,
|
||||
totalAmountOnBoard,
|
||||
totalLABOnBoard,
|
||||
totalLAROnBoard,
|
||||
jobsOnBoard,
|
||||
tasksOnBoard,
|
||||
tasksInProduction
|
||||
]
|
||||
);
|
||||
|
||||
const sortedStatistics = useMemo(() => {
|
||||
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
|
||||
|
||||
return (
|
||||
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
|
||||
).reduce((sorted, orderId) => {
|
||||
const value = statisticsMap.get(orderId);
|
||||
if (value && value.value !== null) {
|
||||
sorted.push(value);
|
||||
}
|
||||
return sorted;
|
||||
}, []);
|
||||
}, [statistics, cardSettings.statisticsOrder]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "5px", flexWrap: "wrap", marginBottom: "5px" }}>
|
||||
{sortedStatistics.map((stat) => (
|
||||
<Card styles={{ body: { padding: "8px" } }} key={stat.id}>
|
||||
<Statistic
|
||||
title={t(`production.statistics.${stat.label}`)}
|
||||
value={formatValue(stat.value, stat.type)}
|
||||
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
|
||||
suffix={
|
||||
stat.type === StatisticType.HOURS
|
||||
? t("production.statistics.hours")
|
||||
: stat.type === StatisticType.JOBS
|
||||
? t("production.statistics.jobs")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductionStatistics.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
cardSettings: PropTypes.object.isRequired,
|
||||
reducerData: PropTypes.object
|
||||
};
|
||||
|
||||
export default ProductionStatistics;
|
||||
@@ -1,145 +1,72 @@
|
||||
.react-kanban-board {
|
||||
.react-trello-board {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-card {
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 4px;
|
||||
margin-bottom: 7px;
|
||||
.height-preserving-container:empty {
|
||||
min-height: calc(var(--child-height));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// .react-kanban-card-skeleton,
|
||||
// .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;
|
||||
.height-preserving-container {
|
||||
}
|
||||
|
||||
.react-kanban-card__description {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.react-kanban-card__title {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
.react-trello-column-header {
|
||||
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;
|
||||
border: 1px solid #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-button:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__title {
|
||||
font-weight: bold;
|
||||
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;
|
||||
.production-alert {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 45%;
|
||||
margin-top: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.react-trello-footer {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__button:hover {
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
background-color: #ccc;
|
||||
.grid-item {
|
||||
margin: 1px; // TODO: (Note) THis is where we set the margin for vertical
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
.lane-title {
|
||||
vertical-align: middle;
|
||||
|
||||
.icon {
|
||||
margin-right: 8px; /* Adjust the spacing as needed */
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-header input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button {
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button:hover,
|
||||
.react-kanban-column-header__button:focus,
|
||||
.react-kanban-column-header__button:active {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button {
|
||||
border: 2px dashed #eee;
|
||||
height: 132px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button:hover {
|
||||
cursor: pointer;
|
||||
.header-content-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.body-empty-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.tech-container {
|
||||
font-weight: bolder;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
.branches-outlined {
|
||||
color: orangered;
|
||||
}
|
||||
}
|
||||
.inner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
.circle-outline {
|
||||
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";
|
||||
|
||||
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";
|
||||
const sortedList = [];
|
||||
const byParentsIdsList = groupBy(arr, "kanbanparent"); // Create a new array with objects indexed by parentId
|
||||
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
|
||||
const byParentsIdsList = groupBy(arr, "kanbanparent");
|
||||
|
||||
while (byParentsIdsList[parentId]) {
|
||||
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
|
||||
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
|
||||
sortedList.push(...byParentsIdsList[parentId]);
|
||||
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) {
|
||||
arr.map((origItem) => {
|
||||
if (!!!sortedList.find((s) => s.id === origItem.id)) {
|
||||
arr.forEach((origItem) => {
|
||||
if (!sortedList.some((s) => s.id === origItem.id)) {
|
||||
sortedList.push(origItem);
|
||||
console.log("DATA CONSISTENCY ERROR: ", origItem.ro_number);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
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 boardLanes = {
|
||||
columns: AllStatuses.map((s) => {
|
||||
return {
|
||||
id: s,
|
||||
title: s,
|
||||
cards: []
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const filteredJobs =
|
||||
(search === "" || !search) && !employeeId
|
||||
? Jobs
|
||||
: Jobs.filter((j) => {
|
||||
let include = false;
|
||||
if (search && search !== "") {
|
||||
include = CheckSearch(search, j);
|
||||
}
|
||||
const lanes = statuses.map((status) => ({
|
||||
id: status,
|
||||
title: status,
|
||||
cards: []
|
||||
}));
|
||||
|
||||
if (!!employeeId) {
|
||||
include =
|
||||
include ||
|
||||
j.employee_body === employeeId ||
|
||||
j.employee_prep === employeeId ||
|
||||
j.employee_csr === employeeId ||
|
||||
j.employee_refinish === employeeId;
|
||||
}
|
||||
let filteredJobs =
|
||||
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||
|
||||
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 {
|
||||
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
|
||||
if (!needle?.cards) return null;
|
||||
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
|
||||
const lane = lanes.find((l) => l.id === statusGroupKey);
|
||||
if (!lane) return;
|
||||
|
||||
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
|
||||
const { id, title, description, due_date, ...metadata } = job;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
label: due_date || "",
|
||||
metadata
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.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) => {
|
||||
return (
|
||||
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
// Function to check if a job matches the search and/or employeeId filter
|
||||
const checkFilter = (search, employeeId, job) => {
|
||||
const lowerSearch = search?.toLowerCase() ?? "";
|
||||
|
||||
const matchesSearch =
|
||||
lowerSearch &&
|
||||
[
|
||||
job.ro_number,
|
||||
job.ownr_fn,
|
||||
job.ownr_co_nm,
|
||||
job.ownr_ln,
|
||||
job.status,
|
||||
job.v_make_desc,
|
||||
job.v_model_desc,
|
||||
job.clm_no,
|
||||
job.plate_no
|
||||
].some((field) => field?.toLowerCase().includes(lowerSearch));
|
||||
|
||||
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,38 @@
|
||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const InformationSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.information")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
"model_info",
|
||||
"ownr_nm",
|
||||
"clm_no",
|
||||
"ins_co_nm",
|
||||
"employeeassignments",
|
||||
"actual_in",
|
||||
"scheduled_completion",
|
||||
"ats",
|
||||
"production_note",
|
||||
"sublets",
|
||||
"partsstatus",
|
||||
"estimator",
|
||||
"subtotal",
|
||||
"tasks"
|
||||
].map((item) => (
|
||||
<Col span={4} key={item}>
|
||||
<Form.Item name={item} valuePropName="checked">
|
||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
InformationSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InformationSettings;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Card, Col, Form, Radio, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const LayoutSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.layout")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
{
|
||||
name: "orientation",
|
||||
label: t("production.labels.orientation"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.vertical") },
|
||||
{ value: false, label: t("production.labels.horizontal") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardSize",
|
||||
label: t("production.labels.card_size"),
|
||||
options: [
|
||||
{ value: "small", label: t("production.options.small") },
|
||||
{ value: "medium", label: t("production.options.medium") },
|
||||
{ value: "large", label: t("production.options.large") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "compact",
|
||||
label: t("production.labels.compact"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.tall") },
|
||||
{ value: false, label: t("production.labels.wide") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardcolor",
|
||||
label: t("production.labels.cardcolor"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "kiosk",
|
||||
label: t("production.labels.kiosk_mode"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
}
|
||||
].map(({ name, label, options }) => (
|
||||
<Col span={4} key={name}>
|
||||
<Form.Item name={name} label={label}>
|
||||
<Radio.Group>
|
||||
{options.map((option) => (
|
||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||
{option.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
LayoutSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LayoutSettings;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||
import { Card, Checkbox, Form } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||
const onDragEnd = (result) => {
|
||||
if (!result.destination) return;
|
||||
const newOrder = Array.from(statisticsOrder);
|
||||
const [movedItem] = newOrder.splice(result.source.index, 1);
|
||||
newOrder.splice(result.destination.index, 0, movedItem);
|
||||
setStatisticsOrder(newOrder);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("production.settings.statistics_title")}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable direction="grid" droppableId="statistics">
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}
|
||||
>
|
||||
{statisticsOrder.map((itemId, index) => {
|
||||
const item = statisticsItems.find((stat) => stat.id === itemId);
|
||||
return (
|
||||
<Draggable key={itemId} draggableId={itemId.toString()} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Card styles={{ body: { padding: "5px" } }} style={{ marginBottom: 8, flex: "0 1 auto" }}>
|
||||
<Form.Item style={{ marginBottom: 0 }} name={item.name} valuePropName="checked">
|
||||
<Checkbox>{t(`production.settings.statistics.${item.label}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
statisticsOrder: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
setStatisticsOrder: PropTypes.func.isRequired,
|
||||
setHasChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StatisticsSettings;
|
||||
@@ -0,0 +1,67 @@
|
||||
const statisticsItems = [
|
||||
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
|
||||
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
|
||||
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
|
||||
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
|
||||
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
||||
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
||||
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
||||
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||
];
|
||||
|
||||
const defaultKanbanSettings = {
|
||||
ats: true,
|
||||
clm_no: true,
|
||||
compact: false,
|
||||
ownr_nm: true,
|
||||
sublets: true,
|
||||
ins_co_nm: true,
|
||||
production_note: true,
|
||||
employeeassignments: true,
|
||||
scheduled_completion: true,
|
||||
cardcolor: false,
|
||||
orientation: false,
|
||||
tasks: false,
|
||||
cardSize: "small",
|
||||
model_info: true,
|
||||
kiosk: false,
|
||||
totalHrs: true,
|
||||
totalAmountInProduction: false,
|
||||
totalLAB: true,
|
||||
totalLAR: true,
|
||||
jobsInProduction: true,
|
||||
totalHrsOnBoard: false,
|
||||
totalLABOnBoard: false,
|
||||
totalLAROnBoard: false,
|
||||
jobsOnBoard: false,
|
||||
tasksOnBoard: false,
|
||||
tasksInProduction: false,
|
||||
totalAmountOnBoard: true,
|
||||
estimator: false,
|
||||
subtotal: false,
|
||||
statisticsOrder: statisticsItems.map((item) => item.id),
|
||||
selectedMdInsCos: [],
|
||||
selectedEstimators: []
|
||||
};
|
||||
|
||||
const mergeWithDefaults = (settings) => {
|
||||
// Create a new object that starts with the default settings
|
||||
const mergedSettings = { ...defaultKanbanSettings };
|
||||
|
||||
// Override with the provided settings, if any
|
||||
if (settings) {
|
||||
for (const key in settings) {
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
mergedSettings[key] = settings[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedSettings;
|
||||
};
|
||||
|
||||
export { defaultKanbanSettings, statisticsItems, mergeWithDefaults };
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, notification, Popover, Row, Tabs } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||
import LayoutSettings from "./LayoutSettings.jsx";
|
||||
import InformationSettings from "./InformationSettings.jsx";
|
||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||
import FilterSettings from "./FilterSettings.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [statisticsOrder, setStatisticsOrder] = useState(defaultKanbanSettings.statisticsOrder);
|
||||
const [selectedMdInsCos, setSelectedMdInsCos] = useState(defaultKanbanSettings.selectedMdInsCos);
|
||||
const [selectedEstimators, setSelectedEstimators] = useState(defaultKanbanSettings.selectedEstimators);
|
||||
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (associationSettings?.kanban_settings) {
|
||||
const finalSettings = mergeWithDefaults(associationSettings.kanban_settings);
|
||||
form.setFieldsValue(finalSettings);
|
||||
setStatisticsOrder(finalSettings.statisticsOrder);
|
||||
setSelectedMdInsCos(finalSettings.selectedMdInsCos);
|
||||
setSelectedEstimators(finalSettings.selectedEstimators);
|
||||
}
|
||||
}, [form, associationSettings]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
parentLoading(true);
|
||||
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: associationSettings?.id,
|
||||
ks: {
|
||||
...associationSettings.kanban_settings,
|
||||
...values,
|
||||
statisticsOrder,
|
||||
selectedMdInsCos,
|
||||
selectedEstimators
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.errors) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("production.errors.settings", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
parentLoading(false);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleValuesChange = () => setHasChanges(true);
|
||||
|
||||
const handleRestoreDefaults = () => {
|
||||
form.setFieldsValue({
|
||||
...defaultKanbanSettings,
|
||||
statisticsOrder: defaultKanbanSettings.statisticsOrder
|
||||
});
|
||||
setStatisticsOrder(defaultKanbanSettings.statisticsOrder);
|
||||
setSelectedMdInsCos(defaultKanbanSettings.selectedMdInsCos);
|
||||
setSelectedEstimators(defaultKanbanSettings.selectedEstimators);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<Card style={{ minWidth: "80vw" }}>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: t("production.settings.layout"),
|
||||
children: <LayoutSettings t={t} />
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: t("production.settings.information"),
|
||||
children: <InformationSettings t={t} />
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: t("production.settings.statistics_title"),
|
||||
children: (
|
||||
<StatisticsSettings
|
||||
t={t}
|
||||
statisticsOrder={statisticsOrder}
|
||||
setStatisticsOrder={setStatisticsOrder}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
label: t("production.settings.filters_title"),
|
||||
children: (
|
||||
<FilterSettings
|
||||
selectedMdInsCos={selectedMdInsCos}
|
||||
setSelectedMdInsCos={setSelectedMdInsCos}
|
||||
selectedEstimators={selectedEstimators}
|
||||
setSelectedEstimators={setSelectedEstimators}
|
||||
setHasChanges={setHasChanges}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
|
||||
<Col span={8}>
|
||||
<Button block onClick={() => setOpen(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button block onClick={handleRestoreDefaults}>
|
||||
{t("general.actions.defaults")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button block onClick={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(!open)}>
|
||||
{t("production.settings.board_settings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
ProductionBoardKanbanSettings.propTypes = {
|
||||
associationSettings: PropTypes.object,
|
||||
parentLoading: PropTypes.func.isRequired,
|
||||
bodyshop: PropTypes.object.isRequired,
|
||||
data: PropTypes.array
|
||||
};
|
||||
|
||||
export default ProductionBoardKanbanSettings;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Height Memory Wrapper
|
||||
* @param children
|
||||
* @param maxHeight
|
||||
* @param setMaxHeight
|
||||
* @param override - Override the minHeight style from being set
|
||||
* @param itemKey - Unique key to preserve height for items with the same key
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, itemKey }) => {
|
||||
const ref = useRef(null);
|
||||
const heightMapRef = useRef(new Map());
|
||||
const [localMaxHeight, setLocalMaxHeight] = useState(maxHeight);
|
||||
const [devicePixelRatio, setDevicePixelRatio] = useState(window.devicePixelRatio);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = ref.current;
|
||||
const updateHeight = () => {
|
||||
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||
if (itemKey) {
|
||||
const keyHeight = heightMapRef.current.get(itemKey) || 0;
|
||||
const newHeight = Math.max(keyHeight, currentHeight);
|
||||
heightMapRef.current.set(itemKey, newHeight);
|
||||
setLocalMaxHeight(newHeight);
|
||||
} else {
|
||||
setLocalMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
}
|
||||
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.observe(currentRef.firstChild);
|
||||
}
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (Math.abs(window.devicePixelRatio - devicePixelRatio) > 0.1) {
|
||||
// Threshold to detect significant zoom level change
|
||||
heightMapRef.current.clear(); // Clearing the height memory as zoom level has changed significantly
|
||||
setLocalMaxHeight(0); // Reset local max height
|
||||
setDevicePixelRatio(window.devicePixelRatio); // Update the recorded device pixel ratio
|
||||
}
|
||||
updateHeight();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
return () => {
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.unobserve(currentRef.firstChild);
|
||||
}
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [itemKey, setMaxHeight, devicePixelRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemKey && heightMapRef.current.has(itemKey)) {
|
||||
setLocalMaxHeight(heightMapRef.current.get(itemKey));
|
||||
}
|
||||
}, [itemKey]);
|
||||
|
||||
const style = override ? {} : { minHeight: localMaxHeight };
|
||||
|
||||
return (
|
||||
<div ref={ref} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HeightMemoryWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
maxHeight: PropTypes.number.isRequired,
|
||||
setMaxHeight: PropTypes.func.isRequired,
|
||||
override: PropTypes.bool,
|
||||
itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
};
|
||||
|
||||
export default HeightMemoryWrapper;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const HeightPreservingItem = ({ children, ...props }) => {
|
||||
const [size, setSize] = useState(0);
|
||||
const knownSize = props["data-known-size"];
|
||||
useEffect(() => {
|
||||
setSize((prevSize) => {
|
||||
return knownSize === 0 ? prevSize : knownSize;
|
||||
});
|
||||
}, [setSize, knownSize]);
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className="height-preserving-container"
|
||||
style={{
|
||||
"--child-height": `${size}px`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeightPreservingItem;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
const ItemComponent = ({ children, maxCardHeight, maxCardWidth, ...props }) => (
|
||||
<div style={{ minWidth: maxCardWidth, minHeight: maxCardHeight }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ItemComponent;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
const ItemWrapper = React.memo(({ children, ...props }) => (
|
||||
<div {...props} className="item-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default ItemWrapper;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { LaneFooter } from "../styles/Base.js";
|
||||
import { CollapseBtn, ExpandBtn } from "../styles/Elements.js";
|
||||
|
||||
const LaneFooterComponent = ({ onClick, collapsed }) => (
|
||||
<LaneFooter className="react-trello-footer" onClick={onClick}>
|
||||
{collapsed ? <ExpandBtn /> : <CollapseBtn />}
|
||||
</LaneFooter>
|
||||
);
|
||||
|
||||
export default LaneFooterComponent;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
const ListComponent = forwardRef(({ style, children, ...props }, ref) => (
|
||||
<div ref={ref} {...props} style={{ ...style }}>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default ListComponent;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SizeMemoryWrapper = ({ children, maxHeight, setMaxHeight, maxWidth, setMaxWidth }) => {
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = ref.current;
|
||||
|
||||
const updateSize = () => {
|
||||
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||
const currentWidth = currentRef?.firstChild?.clientWidth || 0;
|
||||
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
setMaxWidth((prevWidth) => Math.max(prevWidth, currentWidth));
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateSize);
|
||||
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.observe(currentRef.firstChild);
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
if (window.devicePixelRatio < 1) {
|
||||
return; // Do not update width and height
|
||||
}
|
||||
updateSize();
|
||||
};
|
||||
|
||||
window.addEventListener("load", handleLoad);
|
||||
|
||||
return () => {
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.unobserve(currentRef.firstChild);
|
||||
}
|
||||
window.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}, [setMaxHeight, setMaxWidth]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="size-memory-wrapper" style={{ minHeight: maxHeight, minWidth: maxWidth }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SizeMemoryWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
maxHeight: PropTypes.number.isRequired,
|
||||
setMaxHeight: PropTypes.func.isRequired,
|
||||
maxWidth: PropTypes.number.isRequired,
|
||||
setMaxWidth: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SizeMemoryWrapper;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BoardContainer } from "../index";
|
||||
import { useMemo } from "react";
|
||||
import { StyleHorizontal, StyleVertical } from "../styles/Base.js";
|
||||
import { cardSizesVertical } from "../styles/Globals.js";
|
||||
|
||||
const Board = ({ id, className, orientation, cardSettings, ...additionalProps }) => {
|
||||
const OrientationStyle = useMemo(
|
||||
() => (orientation === "horizontal" ? StyleHorizontal : StyleVertical),
|
||||
[orientation]
|
||||
);
|
||||
|
||||
const gridItemWidth = useMemo(() => {
|
||||
switch (cardSettings?.cardSize) {
|
||||
case "small":
|
||||
return cardSizesVertical.small;
|
||||
case "large":
|
||||
return cardSizesVertical.large;
|
||||
case "medium":
|
||||
return cardSizesVertical.medium;
|
||||
default:
|
||||
return cardSizesVertical.small;
|
||||
}
|
||||
}, [cardSettings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrientationStyle {...{ gridItemWidth }}>
|
||||
<BoardContainer
|
||||
orientation={orientation}
|
||||
cardSettings={cardSettings}
|
||||
{...additionalProps}
|
||||
className="react-trello-board"
|
||||
/>
|
||||
</OrientationStyle>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Board;
|
||||
@@ -0,0 +1,173 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { DragDropContext } from "../dnd/lib";
|
||||
import PropTypes from "prop-types";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import Lane from "./Lane";
|
||||
import { PopoverWrapper } from "react-popopo";
|
||||
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||
import { BoardWrapper } from "../styles/Base.js";
|
||||
import ProductionStatistics from "../../production-board-kanban.statistics.jsx";
|
||||
|
||||
const useDragMap = () => {
|
||||
const dragMapRef = useRef(new Map());
|
||||
|
||||
const setDragTime = (laneId) => {
|
||||
dragMapRef.current.set(laneId, Date.now());
|
||||
};
|
||||
|
||||
const getLastDragTime = (laneId) => {
|
||||
return dragMapRef.current.get(laneId);
|
||||
};
|
||||
|
||||
return { setDragTime, getLastDragTime };
|
||||
};
|
||||
|
||||
const BoardContainer = ({
|
||||
data,
|
||||
onDataChange = () => {},
|
||||
onDragEnd = () => {},
|
||||
laneSortFunction = () => {},
|
||||
orientation = "horizontal",
|
||||
cardSettings = {},
|
||||
eventBusHandle,
|
||||
reducerData,
|
||||
queryData
|
||||
}) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [maxLaneHeight, setMaxLaneHeight] = useState(0);
|
||||
const [maxCardHeight, setMaxCardHeight] = useState(0);
|
||||
const [maxCardWidth, setMaxCardWidth] = useState(0);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
const { setDragTime, getLastDragTime } = useDragMap();
|
||||
|
||||
const wireEventBus = useCallback(() => {
|
||||
const eventBus = {
|
||||
publish: (event) => {
|
||||
switch (event.type) {
|
||||
// case "ADD_CARD":
|
||||
// return dispatch(actions.addCard({ laneId: event.laneId, card: event.card }));
|
||||
// case "REMOVE_CARD":
|
||||
// return dispatch(actions.removeCard({ laneId: event.laneId, cardId: event.cardId }));
|
||||
// case "REFRESH_BOARD":
|
||||
// return dispatch(actions.loadBoard(event.data));
|
||||
// case "UPDATE_CARDS":
|
||||
// return dispatch(actions.updateCards({ laneId: event.laneId, cards: event.cards }));
|
||||
// case "UPDATE_CARD":
|
||||
// return dispatch(actions.updateCard({ laneId: event.laneId, updatedCard: event.card }));
|
||||
// case "UPDATE_LANES":
|
||||
// return dispatch(actions.updateLanes(event.lanes));
|
||||
// case "UPDATE_LANE":
|
||||
// return dispatch(actions.updateLane(event.lane));
|
||||
case "MOVE_CARD":
|
||||
return dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: event.fromLaneId,
|
||||
toLaneId: event.toLaneId,
|
||||
cardId: event.cardId,
|
||||
index: event.index,
|
||||
event
|
||||
})
|
||||
);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
eventBusHandle(eventBus);
|
||||
}, [dispatch, eventBusHandle]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(actions.loadBoard(data));
|
||||
if (eventBusHandle) {
|
||||
wireEventBus();
|
||||
}
|
||||
}, [data, eventBusHandle, dispatch, wireEventBus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEqual(currentReducerData, reducerData)) {
|
||||
onDataChange(currentReducerData);
|
||||
}
|
||||
}, [currentReducerData, reducerData, onDataChange]);
|
||||
|
||||
const onDragStart = useCallback(() => {
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const onLaneDrag = useCallback(
|
||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||
setIsDragging(false);
|
||||
setDragTime(source.droppableId);
|
||||
if (!type || type !== "lane" || !source || !destination || isEqual(source, destination)) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: destination.droppableId,
|
||||
cardId: draggableId,
|
||||
index: destination.index
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
[dispatch, onDragEnd, setDragTime]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
|
||||
<PopoverWrapper>
|
||||
<BoardWrapper orientation={orientation}>
|
||||
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
|
||||
{currentReducerData.lanes.map((lane, index) => (
|
||||
<Lane
|
||||
key={lane.id}
|
||||
id={lane.id}
|
||||
title={lane.title}
|
||||
index={index}
|
||||
laneSortFunction={laneSortFunction}
|
||||
orientation={orientation}
|
||||
cards={lane.cards}
|
||||
isDragging={isDragging}
|
||||
isProcessing={isProcessing}
|
||||
cardSettings={cardSettings}
|
||||
maxLaneHeight={maxLaneHeight}
|
||||
setMaxLaneHeight={setMaxLaneHeight}
|
||||
maxCardHeight={maxCardHeight}
|
||||
setMaxCardHeight={setMaxCardHeight}
|
||||
maxCardWidth={maxCardWidth}
|
||||
setMaxCardWidth={setMaxCardWidth}
|
||||
lastDrag={getLastDragTime(lane.id)}
|
||||
/>
|
||||
))}
|
||||
</DragDropContext>
|
||||
</BoardWrapper>
|
||||
</PopoverWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BoardContainer.propTypes = {
|
||||
id: PropTypes.string,
|
||||
data: PropTypes.object.isRequired,
|
||||
reducerData: PropTypes.object,
|
||||
onDataChange: PropTypes.func,
|
||||
eventBusHandle: PropTypes.func,
|
||||
laneSortFunction: PropTypes.func,
|
||||
handleDragEnd: PropTypes.func,
|
||||
orientation: PropTypes.string
|
||||
};
|
||||
|
||||
export default BoardContainer;
|
||||
@@ -0,0 +1,291 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import * as actions from "../../../../redux/trello/trello.actions.js";
|
||||
import { Draggable, Droppable } from "../dnd/lib";
|
||||
import { Virtuoso, VirtuosoGrid } from "react-virtuoso";
|
||||
import HeightPreservingItem from "../components/HeightPreservingItem.jsx";
|
||||
import { Section } from "../styles/Base.js";
|
||||
import LaneFooter from "../components/LaneFooter.jsx";
|
||||
import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../../redux/user/user.selectors.js";
|
||||
import { selectTechnician } from "../../../../redux/tech/tech.selectors.js";
|
||||
import ProductionBoardCard from "../../production-board-kanban-card.component.jsx";
|
||||
import HeightMemoryWrapper from "../components/HeightMemoryWrapper.jsx";
|
||||
import SizeMemoryWrapper from "../components/SizeMemoryWrapper.jsx";
|
||||
import ListComponent from "../components/ListComponent.jsx";
|
||||
import ItemComponent from "../components/ItemComponent.jsx";
|
||||
import ItemWrapper from "../components/ItemWrapper.jsx";
|
||||
import objectHash from "object-hash";
|
||||
|
||||
/**
|
||||
* Lane is a React component that represents a lane in a Trello-like board.
|
||||
* @param id
|
||||
* @param title
|
||||
* @param index
|
||||
* @param isProcessing
|
||||
* @param laneSortFunction
|
||||
* @param cards
|
||||
* @param cardSettings
|
||||
* @param orientation
|
||||
* @param maxLaneHeight
|
||||
* @param setMaxLaneHeight
|
||||
* @param maxCardHeight
|
||||
* @param setMaxCardHeight
|
||||
* @param maxCardWidth
|
||||
* @param setMaxCardWidth
|
||||
* @param lastDrag
|
||||
* @param technician -- connected to redux
|
||||
* @param bodyshop -- connected to redux
|
||||
* @returns {Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Lane = ({
|
||||
id,
|
||||
title,
|
||||
index,
|
||||
isProcessing,
|
||||
laneSortFunction,
|
||||
cards,
|
||||
cardSettings = {},
|
||||
orientation = "vertical",
|
||||
maxLaneHeight,
|
||||
setMaxLaneHeight,
|
||||
maxCardHeight,
|
||||
setMaxCardHeight,
|
||||
maxCardWidth,
|
||||
setMaxCardWidth,
|
||||
lastDrag,
|
||||
technician,
|
||||
bodyshop
|
||||
}) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const laneRef = useRef(null);
|
||||
|
||||
const sortedCards = useMemo(() => {
|
||||
if (!cards) return [];
|
||||
if (!laneSortFunction) return cards;
|
||||
return [...cards].sort(laneSortFunction);
|
||||
}, [cards, laneSortFunction]);
|
||||
|
||||
const toggleLaneCollapsed = useCallback(() => {
|
||||
setCollapsed((prevCollapsed) => !prevCollapsed);
|
||||
}, []);
|
||||
|
||||
const renderDraggable = useCallback(
|
||||
(index, card) => {
|
||||
if (!card) {
|
||||
console.log("null card");
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Draggable draggableId={card.id} index={index} key={card.id} isDragDisabled={isProcessing}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={provided.draggableProps.style}
|
||||
className={`item ${snapshot.isDragging ? "is-dragging" : ""}`}
|
||||
key={card.id}
|
||||
>
|
||||
<SizeMemoryWrapper
|
||||
maxHeight={maxCardHeight}
|
||||
setMaxHeight={setMaxCardHeight}
|
||||
maxWidth={maxCardWidth}
|
||||
setMaxWidth={setMaxCardWidth}
|
||||
>
|
||||
<ProductionBoardCard
|
||||
technician={technician}
|
||||
bodyshop={bodyshop}
|
||||
cardSettings={cardSettings}
|
||||
key={card.id}
|
||||
card={card}
|
||||
style={{ minHeight: maxCardHeight, minWidth: maxCardWidth }}
|
||||
className="react-trello-card"
|
||||
/>
|
||||
</SizeMemoryWrapper>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
},
|
||||
[isProcessing, technician, bodyshop, cardSettings, maxCardHeight, setMaxCardHeight, maxCardWidth, setMaxCardWidth]
|
||||
);
|
||||
|
||||
const renderDroppable = useCallback(
|
||||
(provided, renderedCards) => {
|
||||
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
|
||||
const FinalComponent = collapsed ? "div" : Component;
|
||||
const commonProps = {
|
||||
useWindowScroll: true,
|
||||
data: renderedCards
|
||||
};
|
||||
|
||||
const verticalProps = {
|
||||
...commonProps,
|
||||
listClassName: "grid-container",
|
||||
itemClassName: "grid-item",
|
||||
customScrollParent: laneRef.current,
|
||||
components: {
|
||||
List: ListComponent,
|
||||
Item: ItemComponent
|
||||
},
|
||||
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
||||
overscan: { main: 10, reverse: 10 }
|
||||
};
|
||||
|
||||
const horizontalProps = {
|
||||
...commonProps,
|
||||
components: { Item: HeightPreservingItem },
|
||||
overscan: { main: 3, reverse: 3 },
|
||||
itemContent: (index, item) => renderDraggable(index, item),
|
||||
scrollerRef: provided.innerRef,
|
||||
style: {
|
||||
minWidth: maxCardWidth,
|
||||
minHeight: maxLaneHeight
|
||||
}
|
||||
};
|
||||
|
||||
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
||||
|
||||
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
|
||||
// the lane doesn't shrink when collapsed (in horizontal mode)
|
||||
const finalComponentProps = collapsed
|
||||
? orientation === "horizontal"
|
||||
? {
|
||||
style: {
|
||||
height: maxLaneHeight
|
||||
}
|
||||
}
|
||||
: {}
|
||||
: componentProps;
|
||||
|
||||
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
|
||||
// a card is dragged over it
|
||||
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
||||
|
||||
return (
|
||||
<HeightMemoryWrapper
|
||||
itemKey={objectHash({
|
||||
id,
|
||||
orientation,
|
||||
cardSettings,
|
||||
cardLength: renderedCards?.length
|
||||
})}
|
||||
maxHeight={maxLaneHeight}
|
||||
setMaxHeight={setMaxLaneHeight}
|
||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||
>
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||
style={{ ...provided.droppableProps.style }}
|
||||
>
|
||||
<FinalComponent {...finalComponentProps} />
|
||||
{shouldRenderPlaceholder && provided.placeholder}
|
||||
</div>
|
||||
</HeightMemoryWrapper>
|
||||
);
|
||||
},
|
||||
[orientation, collapsed, renderDraggable, maxLaneHeight, setMaxLaneHeight, maxCardWidth, id, cardSettings]
|
||||
);
|
||||
|
||||
const renderDragContainer = useCallback(
|
||||
() => (
|
||||
<Droppable
|
||||
droppableId={id}
|
||||
index={index}
|
||||
type="lane"
|
||||
direction={orientation === "horizontal" ? "vertical" : "grid"}
|
||||
mode="virtual"
|
||||
renderClone={(provided, snapshot, rubric) => {
|
||||
const card = sortedCards[rubric.source.index];
|
||||
return (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={{
|
||||
...provided.draggableProps.style,
|
||||
minHeight: maxCardHeight,
|
||||
minWidth: maxCardWidth
|
||||
}}
|
||||
className={`clone ${snapshot.isDragging ? "is-dragging" : ""}`}
|
||||
key={card.id}
|
||||
>
|
||||
<ProductionBoardCard
|
||||
technician={technician}
|
||||
bodyshop={bodyshop}
|
||||
cardSettings={cardSettings}
|
||||
key={card.id}
|
||||
className="react-trello-card"
|
||||
card={card}
|
||||
clone={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{(provided) => renderDroppable(provided, sortedCards)}
|
||||
</Droppable>
|
||||
),
|
||||
[
|
||||
id,
|
||||
index,
|
||||
orientation,
|
||||
renderDroppable,
|
||||
sortedCards,
|
||||
technician,
|
||||
bodyshop,
|
||||
cardSettings,
|
||||
maxCardHeight,
|
||||
maxCardWidth
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Section key={`lane-${id}-${lastDrag}`} orientation={orientation} cardSettings={cardSettings}>
|
||||
<div onDoubleClick={toggleLaneCollapsed} className="react-trello-column-header">
|
||||
<span className="lane-title">
|
||||
{collapsed ? <EyeInvisibleOutlined className="icon" /> : <EyeOutlined className="icon" />}
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
{renderDragContainer()}
|
||||
<LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
Lane.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
title: PropTypes.node.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
laneSortFunction: PropTypes.func,
|
||||
cards: PropTypes.array.isRequired,
|
||||
orientation: PropTypes.string.isRequired,
|
||||
isProcessing: PropTypes.bool.isRequired,
|
||||
cardSettings: PropTypes.object.isRequired,
|
||||
maxLaneHeight: PropTypes.number.isRequired,
|
||||
setMaxLaneHeight: PropTypes.func.isRequired,
|
||||
maxCardHeight: PropTypes.number.isRequired,
|
||||
setMaxCardHeight: PropTypes.func.isRequired,
|
||||
maxCardWidth: PropTypes.number.isRequired,
|
||||
setMaxCardWidth: PropTypes.func.isRequired,
|
||||
lastDrag: PropTypes.number
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actions, dispatch)
|
||||
});
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Lane);
|
||||
@@ -0,0 +1,67 @@
|
||||
import { isEqual, origin } from "./state/position";
|
||||
|
||||
export const curves = {
|
||||
outOfTheWay: "cubic-bezier(0.2, 0, 0, 1)",
|
||||
drop: "cubic-bezier(.2,1,.1,1)"
|
||||
};
|
||||
export const combine = {
|
||||
opacity: {
|
||||
// while dropping: fade out totally
|
||||
drop: 0,
|
||||
// while dragging: fade out partially
|
||||
combining: 0.7
|
||||
},
|
||||
scale: {
|
||||
drop: 0.75
|
||||
}
|
||||
};
|
||||
export const timings = {
|
||||
outOfTheWay: 0.2,
|
||||
// greater than the out of the way time
|
||||
// so that when the drop ends everything will
|
||||
// have to be out of the way
|
||||
minDropTime: 0.33,
|
||||
maxDropTime: 0.55
|
||||
};
|
||||
|
||||
// slow timings
|
||||
// uncomment to use
|
||||
// export const timings = {
|
||||
// outOfTheWay: 2,
|
||||
// // greater than the out of the way time
|
||||
// // so that when the drop ends everything will
|
||||
// // have to be out of the way
|
||||
// minDropTime: 3,
|
||||
// maxDropTime: 4,
|
||||
// };
|
||||
|
||||
const outOfTheWayTiming = `${timings.outOfTheWay}s ${curves.outOfTheWay}`;
|
||||
export const placeholderTransitionDelayTime = 0.1;
|
||||
export const transitions = {
|
||||
fluid: `opacity ${outOfTheWayTiming}`,
|
||||
snap: `transform ${outOfTheWayTiming}, opacity ${outOfTheWayTiming}`,
|
||||
drop: (duration) => {
|
||||
const timing = `${duration}s ${curves.drop}`;
|
||||
return `transform ${timing}, opacity ${timing}`;
|
||||
},
|
||||
outOfTheWay: `transform ${outOfTheWayTiming}`,
|
||||
placeholder: `height ${outOfTheWayTiming}, width ${outOfTheWayTiming}, margin ${outOfTheWayTiming}`
|
||||
};
|
||||
const moveTo = (offset) => (isEqual(offset, origin) ? null : `translate(${offset.x}px, ${offset.y}px)`);
|
||||
export const transforms = {
|
||||
moveTo,
|
||||
drop: (offset, isCombining) => {
|
||||
const translate = moveTo(offset);
|
||||
if (!translate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// only transforming the translate
|
||||
if (!isCombining) {
|
||||
return translate;
|
||||
}
|
||||
|
||||
// when dropping while combining we also update the scale
|
||||
return `${translate} scale(${combine.scale.drop})`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
const average = (values) => {
|
||||
const sum = values.reduce((previous, current) => previous + current, 0);
|
||||
return sum / values.length;
|
||||
};
|
||||
export default (groupSize) => {
|
||||
console.log("Starting average action timer middleware");
|
||||
console.log(`Will take an average every ${groupSize} actions`);
|
||||
const bucket = {};
|
||||
return () => (next) => (action) => {
|
||||
const start = performance.now();
|
||||
const result = next(action);
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
if (!bucket[action.type]) {
|
||||
bucket[action.type] = [duration];
|
||||
return result;
|
||||
}
|
||||
bucket[action.type].push(duration);
|
||||
if (bucket[action.type].length < groupSize) {
|
||||
return result;
|
||||
}
|
||||
console.warn(`Average time for ${action.type}`, average(bucket[action.type]));
|
||||
|
||||
// reset
|
||||
bucket[action.type] = [];
|
||||
return result;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as timings from "../timings";
|
||||
|
||||
export default () => (next) => (action) => {
|
||||
timings.forceEnable();
|
||||
const key = `redux action: ${action.type}`;
|
||||
timings.start(key);
|
||||
const result = next(action);
|
||||
timings.finish(key);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export default (mode = "verbose") =>
|
||||
(store) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (mode === "light") {
|
||||
console.log("🏃 Action:", action.type);
|
||||
return next(action);
|
||||
}
|
||||
console.group(`action: ${action.type}`);
|
||||
console.log("action payload", action.payload);
|
||||
console.log("state before", store.getState());
|
||||
const result = next(action);
|
||||
console.log("state after", store.getState());
|
||||
console.groupEnd();
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export default () => (next) => (action) => {
|
||||
const title = `👾 redux (action): ${action.type}`;
|
||||
const startMark = `${action.type}:start`;
|
||||
const endMark = `${action.type}:end`;
|
||||
performance.mark(startMark);
|
||||
const result = next(action);
|
||||
performance.mark(endMark);
|
||||
performance.measure(title, startMark, endMark);
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
const records = {};
|
||||
let isEnabled = false;
|
||||
const isTimingsEnabled = () => isEnabled;
|
||||
export const forceEnable = () => {
|
||||
isEnabled = true;
|
||||
};
|
||||
|
||||
// Debug: uncomment to enable
|
||||
// forceEnable();
|
||||
|
||||
export const start = (key) => {
|
||||
// we want to strip all the code out for production builds
|
||||
// draw back: can only do timings in dev env (which seems to be fine for now)
|
||||
if (import.meta.env.DEV) {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
records[key] = now;
|
||||
}
|
||||
};
|
||||
export const finish = (key) => {
|
||||
if (import.meta.env.DEV) {
|
||||
if (!isTimingsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const previous = records[key];
|
||||
if (!previous) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("cannot finish timing as no previous time found", key);
|
||||
return;
|
||||
}
|
||||
const result = now - previous;
|
||||
const rounded = result.toFixed(2);
|
||||
const style = (() => {
|
||||
if (result < 12) {
|
||||
return {
|
||||
textColor: "green",
|
||||
symbol: "✅"
|
||||
};
|
||||
}
|
||||
if (result < 40) {
|
||||
return {
|
||||
textColor: "orange",
|
||||
symbol: "⚠️"
|
||||
};
|
||||
}
|
||||
return {
|
||||
textColor: "red",
|
||||
symbol: "❌"
|
||||
};
|
||||
})();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`${style.symbol} %cTiming %c${rounded} %cms %c${key}`,
|
||||
// title
|
||||
"color: blue; font-weight: bold;",
|
||||
// result
|
||||
`color: ${style.textColor}; font-size: 1.1em;`,
|
||||
// ms
|
||||
"color: grey;",
|
||||
// key
|
||||
"color: purple; font-weight: bold;"
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
const isProduction = import.meta.env.PROD;
|
||||
|
||||
// not replacing newlines (which \s does)
|
||||
const spacesAndTabs = /[ \t]{2,}/g;
|
||||
const lineStartWithSpaces = /^[ \t]*/gm;
|
||||
|
||||
// using .trim() to clear the any newlines before the first text and after last text
|
||||
const clean = (value) => value.replace(spacesAndTabs, " ").replace(lineStartWithSpaces, "").trim();
|
||||
const getDevMessage = (message) =>
|
||||
clean(`
|
||||
%creact-beautiful-dnd
|
||||
|
||||
%c${clean(message)}
|
||||
|
||||
%c👷 This is a development only message. It will be removed in production builds.
|
||||
`);
|
||||
export const getFormattedMessage = (message) => [
|
||||
getDevMessage(message),
|
||||
// title (green400)
|
||||
"color: #00C584; font-size: 1.2em; font-weight: bold;",
|
||||
// message
|
||||
"line-height: 1.5",
|
||||
// footer (purple300)
|
||||
"color: #723874;"
|
||||
];
|
||||
const isDisabledFlag = "__react-beautiful-dnd-disable-dev-warnings";
|
||||
|
||||
export function log(type, message) {
|
||||
// no warnings in production
|
||||
if (isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// manual opt out of warnings
|
||||
if (typeof window !== "undefined" && window[isDisabledFlag]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console[type](...getFormattedMessage(message));
|
||||
}
|
||||
|
||||
export const warning = log.bind(null, "warn");
|
||||
export const error = log.bind(null, "error");
|
||||
@@ -0,0 +1,5 @@
|
||||
export function noop() {}
|
||||
|
||||
export function identity(value) {
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Components
|
||||
export { default as DragDropContext } from "./view/drag-drop-context";
|
||||
export { default as Droppable } from "./view/droppable";
|
||||
export { default as Draggable } from "./view/draggable";
|
||||
|
||||
// Default sensors
|
||||
|
||||
export { useMouseSensor, useTouchSensor, useKeyboardSensor } from "./view/use-sensor-marshal";
|
||||
|
||||
// Utils
|
||||
|
||||
export { resetServerContext } from "./view/drag-drop-context";
|
||||
|
||||
// Public flow types
|
||||
|
||||
// Droppable types
|
||||
|
||||
// Draggable types
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
const isProduction = import.meta.env.PROD;
|
||||
const prefix = "Invariant failed";
|
||||
|
||||
// Want to use this:
|
||||
// export class RbdInvariant extends Error { }
|
||||
// But it causes babel to bring in a lot of code
|
||||
|
||||
export function RbdInvariant(message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
RbdInvariant.prototype.toString = function toString() {
|
||||
return this.message;
|
||||
};
|
||||
|
||||
// A copy-paste of tiny-invariant but with a custom error type
|
||||
// Throw an error if the condition fails
|
||||
export function invariant(condition, message) {
|
||||
if (condition) {
|
||||
return;
|
||||
}
|
||||
if (isProduction) {
|
||||
// In production we strip the message but still throw
|
||||
throw new RbdInvariant(prefix);
|
||||
} else {
|
||||
// When not in production we allow the message to pass through
|
||||
// *This block will be removed in production builds*
|
||||
throw new RbdInvariant(`${prefix}: ${message || ""}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
export function isInteger(value) {
|
||||
if (Number.isInteger) {
|
||||
return Number.isInteger(value);
|
||||
}
|
||||
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
|
||||
}
|
||||
|
||||
// Using this helper to ensure there are correct flow types
|
||||
// https://github.com/facebook/flow/issues/2221
|
||||
export function values(map) {
|
||||
if (Object.values) {
|
||||
// $FlowFixMe - Object.values currently does not have good flow support
|
||||
return Object.values(map);
|
||||
}
|
||||
return Object.keys(map).map((key) => map[key]);
|
||||
}
|
||||
|
||||
// Could also extend to pass index and list
|
||||
|
||||
// TODO: swap order
|
||||
export function findIndex(list, predicate) {
|
||||
if (list.findIndex) {
|
||||
return list.findIndex(predicate);
|
||||
}
|
||||
|
||||
// Using a for loop so that we can exit early
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (predicate(list[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Array.prototype.find returns -1 when nothing is found
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function find(list, predicate) {
|
||||
if (list.find) {
|
||||
return list.find(predicate);
|
||||
}
|
||||
const index = findIndex(list, predicate);
|
||||
if (index !== -1) {
|
||||
return list[index];
|
||||
}
|
||||
// Array.prototype.find returns undefined when nothing is found
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Using this rather than Array.from as Array.from adds 2kb to the gzip
|
||||
// document.querySelector actually returns Element[], but flow thinks it is HTMLElement[]
|
||||
// So we downcast the result to Element[]
|
||||
export function toArray(list) {
|
||||
return Array.prototype.slice.call(list);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const dragHandleUsageInstructions = `
|
||||
Press space bar to start a drag.
|
||||
When dragging you can use the arrow keys to move the item around and escape to cancel.
|
||||
Some screen readers may require you to be in focus mode or to use your pass through key
|
||||
`;
|
||||
const position = (index) => index + 1;
|
||||
|
||||
// We cannot list what index the Droppable is in automatically as we are not sure how
|
||||
// the Droppable's have been configured
|
||||
const onDragStart = (start) => `
|
||||
You have lifted an item in position ${position(start.source.index)}
|
||||
`;
|
||||
const withLocation = (source, destination) => {
|
||||
const isInHomeList = source.droppableId === destination.droppableId;
|
||||
const startPosition = position(source.index);
|
||||
const endPosition = position(destination.index);
|
||||
if (isInHomeList) {
|
||||
return `
|
||||
You have moved the item from position ${startPosition}
|
||||
to position ${endPosition}
|
||||
`;
|
||||
}
|
||||
return `
|
||||
You have moved the item from position ${startPosition}
|
||||
in list ${source.droppableId}
|
||||
to list ${destination.droppableId}
|
||||
in position ${endPosition}
|
||||
`;
|
||||
};
|
||||
const withCombine = (id, source, combine) => {
|
||||
const inHomeList = source.droppableId === combine.droppableId;
|
||||
if (inHomeList) {
|
||||
return `
|
||||
The item ${id}
|
||||
has been combined with ${combine.draggableId}`;
|
||||
}
|
||||
return `
|
||||
The item ${id}
|
||||
in list ${source.droppableId}
|
||||
has been combined with ${combine.draggableId}
|
||||
in list ${combine.droppableId}
|
||||
`;
|
||||
};
|
||||
const onDragUpdate = (update) => {
|
||||
const location = update.destination;
|
||||
if (location) {
|
||||
return withLocation(update.source, location);
|
||||
}
|
||||
const combine = update.combine;
|
||||
if (combine) {
|
||||
return withCombine(update.draggableId, update.source, combine);
|
||||
}
|
||||
return "You are over an area that cannot be dropped on";
|
||||
};
|
||||
const returnedToStart = (source) => `
|
||||
The item has returned to its starting position
|
||||
of ${position(source.index)}
|
||||
`;
|
||||
const onDragEnd = (result) => {
|
||||
if (result.reason === "CANCEL") {
|
||||
return `
|
||||
Movement cancelled.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
}
|
||||
const location = result.destination;
|
||||
const combine = result.combine;
|
||||
if (location) {
|
||||
return `
|
||||
You have dropped the item.
|
||||
${withLocation(result.source, location)}
|
||||
`;
|
||||
}
|
||||
if (combine) {
|
||||
return `
|
||||
You have dropped the item.
|
||||
${withCombine(result.draggableId, result.source, combine)}
|
||||
`;
|
||||
}
|
||||
return `
|
||||
The item has been dropped while not over a drop area.
|
||||
${returnedToStart(result.source)}
|
||||
`;
|
||||
};
|
||||
const preset = {
|
||||
dragHandleUsageInstructions,
|
||||
onDragStart,
|
||||
onDragUpdate,
|
||||
onDragEnd
|
||||
};
|
||||
export default preset;
|
||||
@@ -0,0 +1,88 @@
|
||||
export const beforeInitialCapture = (args) => ({
|
||||
type: "BEFORE_INITIAL_CAPTURE",
|
||||
payload: args
|
||||
});
|
||||
export const lift = (args) => ({
|
||||
type: "LIFT",
|
||||
payload: args
|
||||
});
|
||||
export const initialPublish = (args) => ({
|
||||
type: "INITIAL_PUBLISH",
|
||||
payload: args
|
||||
});
|
||||
export const publishWhileDragging = (args) => ({
|
||||
type: "PUBLISH_WHILE_DRAGGING",
|
||||
payload: args
|
||||
});
|
||||
export const collectionStarting = () => ({
|
||||
type: "COLLECTION_STARTING",
|
||||
payload: null
|
||||
});
|
||||
export const updateDroppableScroll = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const updateDroppableIsEnabled = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_IS_ENABLED",
|
||||
payload: args
|
||||
});
|
||||
export const updateDroppableIsCombineEnabled = (args) => ({
|
||||
type: "UPDATE_DROPPABLE_IS_COMBINE_ENABLED",
|
||||
payload: args
|
||||
});
|
||||
export const move = (args) => ({
|
||||
type: "MOVE",
|
||||
payload: args
|
||||
});
|
||||
export const moveByWindowScroll = (args) => ({
|
||||
type: "MOVE_BY_WINDOW_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const updateViewportMaxScroll = (args) => ({
|
||||
type: "UPDATE_VIEWPORT_MAX_SCROLL",
|
||||
payload: args
|
||||
});
|
||||
export const moveUp = () => ({
|
||||
type: "MOVE_UP",
|
||||
payload: null
|
||||
});
|
||||
export const moveDown = () => ({
|
||||
type: "MOVE_DOWN",
|
||||
payload: null
|
||||
});
|
||||
export const moveRight = () => ({
|
||||
type: "MOVE_RIGHT",
|
||||
payload: null
|
||||
});
|
||||
export const moveLeft = () => ({
|
||||
type: "MOVE_LEFT",
|
||||
payload: null
|
||||
});
|
||||
export const flush = () => ({
|
||||
type: "FLUSH",
|
||||
payload: null
|
||||
});
|
||||
export const animateDrop = (args) => ({
|
||||
type: "DROP_ANIMATE",
|
||||
payload: args
|
||||
});
|
||||
export const completeDrop = (args) => ({
|
||||
type: "DROP_COMPLETE",
|
||||
payload: args
|
||||
});
|
||||
export const drop = (args) => ({
|
||||
type: "DROP",
|
||||
payload: args
|
||||
});
|
||||
export const cancel = () =>
|
||||
drop({
|
||||
reason: "CANCEL"
|
||||
});
|
||||
export const dropPending = (args) => ({
|
||||
type: "DROP_PENDING",
|
||||
payload: args
|
||||
});
|
||||
export const dropAnimationFinished = () => ({
|
||||
type: "DROP_ANIMATION_FINISHED",
|
||||
payload: null
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { add, apply, isEqual, origin } from "../position";
|
||||
|
||||
const smallestSigned = apply((value) => {
|
||||
if (value === 0) {
|
||||
return 0;
|
||||
}
|
||||
return value > 0 ? 1 : -1;
|
||||
});
|
||||
// We need to figure out how much of the movement
|
||||
// cannot be done with a scroll
|
||||
export const getOverlap = (() => {
|
||||
const getRemainder = (target, max) => {
|
||||
if (target < 0) {
|
||||
return target;
|
||||
}
|
||||
if (target > max) {
|
||||
return target - max;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return ({ current, max, change }) => {
|
||||
const targetScroll = add(current, change);
|
||||
const overlap = {
|
||||
x: getRemainder(targetScroll.x, max.x),
|
||||
y: getRemainder(targetScroll.y, max.y)
|
||||
};
|
||||
if (isEqual(overlap, origin)) {
|
||||
return null;
|
||||
}
|
||||
return overlap;
|
||||
};
|
||||
})();
|
||||
export const canPartiallyScroll = ({ max: rawMax, current, change }) => {
|
||||
// It is possible for the max scroll to be greater than the current scroll
|
||||
// when there are scrollbars on the cross axis. We adjust for this by
|
||||
// increasing the max scroll point if needed
|
||||
// This will allow movements backwards even if the current scroll is greater than the max scroll
|
||||
const max = {
|
||||
x: Math.max(current.x, rawMax.x),
|
||||
y: Math.max(current.y, rawMax.y)
|
||||
};
|
||||
|
||||
// Only need to be able to move the smallest amount in the desired direction
|
||||
const smallestChange = smallestSigned(change);
|
||||
const overlap = getOverlap({
|
||||
max,
|
||||
current,
|
||||
change: smallestChange
|
||||
});
|
||||
|
||||
// no overlap at all - we can move there!
|
||||
if (!overlap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there was an x value, but there is no x overlap - then we can scroll on the x!
|
||||
if (smallestChange.x !== 0 && overlap.x === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if there was an y value, but there is no y overlap - then we can scroll on the y!
|
||||
if (smallestChange.y !== 0 && overlap.y === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const canScrollWindow = (viewport, change) =>
|
||||
canPartiallyScroll({
|
||||
current: viewport.scroll.current,
|
||||
max: viewport.scroll.max,
|
||||
change
|
||||
});
|
||||
export const getWindowOverlap = (viewport, change) => {
|
||||
if (!canScrollWindow(viewport, change)) {
|
||||
return null;
|
||||
}
|
||||
const max = viewport.scroll.max;
|
||||
const current = viewport.scroll.current;
|
||||
return getOverlap({
|
||||
current,
|
||||
max,
|
||||
change
|
||||
});
|
||||
};
|
||||
export const canScrollDroppable = (droppable, change) => {
|
||||
const frame = droppable.frame;
|
||||
|
||||
// Cannot scroll when there is no scrollable
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
return canPartiallyScroll({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change
|
||||
});
|
||||
};
|
||||
export const getDroppableOverlap = (droppable, change) => {
|
||||
const frame = droppable.frame;
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
if (!canScrollDroppable(droppable, change)) {
|
||||
return null;
|
||||
}
|
||||
return getOverlap({
|
||||
current: frame.scroll.current,
|
||||
max: frame.scroll.max,
|
||||
change
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
// Values used to control how the fluid auto scroll feels
|
||||
const config = {
|
||||
// percentage distance from edge of container:
|
||||
startFromPercentage: 0.25,
|
||||
maxScrollAtPercentage: 0.05,
|
||||
// pixels per frame
|
||||
maxPixelScroll: 28,
|
||||
// A function used to ease a percentage value
|
||||
// A simple linear function would be: (percentage) => percentage;
|
||||
// percentage is between 0 and 1
|
||||
// result must be between 0 and 1
|
||||
ease: (percentage) => Math.pow(percentage, 2),
|
||||
durationDampening: {
|
||||
// ms: how long to dampen the speed of an auto scroll from the start of a drag
|
||||
stopDampeningAt: 1200,
|
||||
// ms: when to start accelerating the reduction of duration dampening
|
||||
accelerateAt: 360
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user