- Merge master-aio, bump packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
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
|
||||
@@ -1,174 +1,177 @@
|
||||
<!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 || []);
|
||||
|
||||
<% } %>
|
||||
<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);
|
||||
})();
|
||||
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>
|
||||
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
!(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>
|
||||
</html>
|
||||
|
||||
1249
client/package-lock.json
generated
1249
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,22 +8,22 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/pro-layout": "^7.19.11",
|
||||
"@apollo/client": "^3.10.8",
|
||||
"@ant-design/pro-layout": "^7.19.12",
|
||||
"@apollo/client": "^3.11.4",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@sentry/cli": "^2.32.2",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@sentry/cli": "^2.33.1",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.1",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.19.3",
|
||||
"antd": "^5.20.2",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.6.8",
|
||||
"axios": "^1.7.4",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
@@ -32,13 +32,13 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.12.4",
|
||||
"firebase": "^10.13.0",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.12.2",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.11.4",
|
||||
"logrocket": "^8.1.1",
|
||||
"libphonenumber-js": "^1.11.7",
|
||||
"logrocket": "^8.1.2",
|
||||
"markerjs2": "^2.32.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
@@ -47,7 +47,7 @@
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.1",
|
||||
"react-big-calendar": "^1.13.3",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -55,19 +55,18 @@
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-icons": "^5.2.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-joyride": "^2.8.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.4.0",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.6",
|
||||
"react-product-fruits": "^2.2.61",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.7.13",
|
||||
"react-virtuoso": "^4.10.1",
|
||||
"recharts": "^2.12.7",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
@@ -80,7 +79,7 @@
|
||||
"styled-components": "^6.1.12",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.2",
|
||||
"userpilot": "^1.3.5",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
@@ -131,29 +130,29 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@dotenvx/dotenvx": "^1.6.4",
|
||||
"@dotenvx/dotenvx": "^1.7.0",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@sentry/webpack-plugin": "^2.21.1",
|
||||
"@sentry/webpack-plugin": "^2.22.2",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.23.2",
|
||||
"browserslist": "^4.23.3",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.13.1",
|
||||
"cypress": "^13.13.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.9.3",
|
||||
"memfs": "^4.11.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.3.4",
|
||||
"vite": "^5.4.1",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-style-import": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { checkUserSession } from "../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
|
||||
import PrivateRoute from "../components/PrivateRoute";
|
||||
import "./App.styles.scss";
|
||||
import handleBeta from "../utils/betaHandler";
|
||||
import handleBeta from "../utils/handleBeta";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ ProductFruitsWrapper.propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}).isRequired,
|
||||
workspaceCode: PropTypes.string.isRequired
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
|
||||
@@ -170,13 +171,22 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
extra={
|
||||
<Space wrap>
|
||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<>
|
||||
<JobMarkSelectedExported
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||
<Input.Search
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -36,9 +36,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Scheduled Out Today");
|
||||
console.dir(scheduledOutToday);
|
||||
|
||||
const tvFontSize = 18;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ 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 { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,27 +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 TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
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",
|
||||
@@ -47,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>
|
||||
@@ -84,17 +93,17 @@ 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>
|
||||
)
|
||||
@@ -111,6 +120,7 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
<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
|
||||
@@ -119,9 +129,15 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
{!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>
|
||||
|
||||
@@ -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,19 +31,20 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import { 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";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -54,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" }))
|
||||
});
|
||||
@@ -63,6 +65,7 @@ export function JobLinesComponent({
|
||||
jobRO,
|
||||
technician,
|
||||
setPartsOrderContext,
|
||||
setPartsReceiveContext,
|
||||
loading,
|
||||
refetch,
|
||||
jobLines,
|
||||
@@ -71,7 +74,11 @@ export function JobLinesComponent({
|
||||
setJobLineEditContext,
|
||||
form,
|
||||
setBillEnterContext,
|
||||
setTaskUpsertContext
|
||||
setTaskUpsertContext,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||
const {
|
||||
@@ -198,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",
|
||||
@@ -213,7 +219,6 @@ export function JobLinesComponent({
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
|
||||
// {
|
||||
// title: t('joblines.fields.tax_part'),
|
||||
// dataIndex: 'tax_part',
|
||||
@@ -322,7 +327,7 @@ export function JobLinesComponent({
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -337,7 +342,6 @@ export function JobLinesComponent({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
@@ -351,7 +355,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -437,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={
|
||||
@@ -553,7 +566,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
{InstanceRenderManager({ rome: <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,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"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
@@ -37,7 +38,10 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sorter: search?.search
|
||||
? (a, b) =>
|
||||
parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((b.ro_number || "0").replace(/\D/g, ""))
|
||||
: true,
|
||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.id}>{record.ro_number || t("general.labels.na")}</Link>
|
||||
@@ -49,7 +53,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "ownr_ln",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
@@ -67,7 +70,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ownr_ph1"),
|
||||
dataIndex: "ownr_ph1",
|
||||
key: "ownr_ph1",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <StartChatButton phone={record.ownr_ph1} jobid={record.id} />
|
||||
},
|
||||
@@ -75,7 +77,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.ownr_ph2"),
|
||||
dataIndex: "ownr_ph2",
|
||||
key: "ownr_ph2",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <StartChatButton phone={record.ownr_ph2} jobid={record.id} />
|
||||
},
|
||||
@@ -85,7 +86,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
key: "status",
|
||||
|
||||
ellipsis: true,
|
||||
sorter: true, // (a, b) => alphaSort(a.status, b.status),
|
||||
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
|
||||
sortOrder: sortcolumn === "status" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.status || t("general.labels.na");
|
||||
@@ -100,7 +101,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => {
|
||||
return record.vehicleid ? (
|
||||
@@ -117,7 +117,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "plate_no",
|
||||
key: "plate_no",
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||
sorter: search?.search ? (a, b) => alphaSort(a.plate_no, b.plate_no) : true,
|
||||
sortOrder: sortcolumn === "plate_no" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.plate_no ? record.plate_no : "";
|
||||
@@ -128,7 +128,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
dataIndex: "clm_no",
|
||||
key: "clm_no",
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sorter: search?.search ? (a, b) => alphaSort(a.clm_no, b.clm_no) : true,
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
|
||||
},
|
||||
@@ -142,8 +142,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
key: "clm_total",
|
||||
|
||||
sorter: true, //(a, b) => a.clm_total - b.clm_total,
|
||||
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
|
||||
sortOrder: sortcolumn === "clm_total" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.clm_total ? (
|
||||
@@ -157,7 +156,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
title: t("jobs.fields.owner_owing"),
|
||||
dataIndex: "owner_owing",
|
||||
key: "owner_owing",
|
||||
|
||||
render: (text, record) => <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
|
||||
@@ -16,18 +16,15 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
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";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps))
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
export function JobsList({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { selected } = searchParams;
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, notification, Popconfirm } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOBS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobMarkSelectedExported);
|
||||
|
||||
export function JobMarkSelectedExported({
|
||||
bodyshop,
|
||||
currentUser,
|
||||
jobIds,
|
||||
disabled,
|
||||
loadingCallback,
|
||||
completedCallback,
|
||||
refetch,
|
||||
insertAuditTrail
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
|
||||
const [updateJob] = useMutation(UPDATE_JOBS);
|
||||
const handleUpdate = async () => {
|
||||
setLoading(true);
|
||||
loadingCallback(true);
|
||||
const result = await updateJob({
|
||||
variables: {
|
||||
jobIds: jobIds,
|
||||
fields: {
|
||||
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
|
||||
date_exported: new Date()
|
||||
}
|
||||
},
|
||||
update(cache) {}
|
||||
});
|
||||
|
||||
await insertExportLog({
|
||||
variables: {
|
||||
logs: jobIds.map((id) => {
|
||||
return {
|
||||
bodyshopid: bodyshop.id,
|
||||
jobid: id,
|
||||
successful: true,
|
||||
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||
useremail: currentUser.email
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.errors) {
|
||||
notification["success"]({ message: t("jobs.successes.save") });
|
||||
result.data.update_jobs.returning.forEach((job) => {
|
||||
console.log("results job", job.id, "audit: ", AuditTrailMapping.admin_jobmarkexported());
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.admin_jobmarkexported(),
|
||||
type: "admin_jobmarkexported"
|
||||
});
|
||||
});
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
loadingCallback(false);
|
||||
completedCallback && completedCallback([]);
|
||||
setLoading(false);
|
||||
refetch && refetch();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
open={open}
|
||||
title={t("general.labels.areyousure")}
|
||||
onCancel={() => setOpen(false)}
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
|
||||
{t("jobs.actions.markasexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPartsReceiveContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsReceive"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function PartsOrderListTableDrawerComponent({
|
||||
setBillEnterContext,
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "75%",
|
||||
xl: "75%",
|
||||
xxl: "65%"
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
const Templates = TemplateList("partsorder", { job });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill }
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
setPartsReceiveContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
partsorderlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
id: pol.id,
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
act_price: pol.act_price,
|
||||
oem_partno: pol.oem_partno
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receive")}
|
||||
</Button>
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
partsorderid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("parts_orders.labels.confirmdelete")}
|
||||
disabled={jobRO}
|
||||
onConfirm={async () => {
|
||||
//Delete the parts return.!
|
||||
|
||||
await deletePartsOrder({
|
||||
variables: { partsOrderId: record.id },
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
parts_orders(existingPartsOrders, { readField }) {
|
||||
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button disabled={jobRO}>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
|
||||
<Button
|
||||
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
job: job,
|
||||
bill: {
|
||||
vendorid: record.vendor.id,
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
actual_price: pol.act_price,
|
||||
|
||||
cost_center: pol.jobline?.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? pol.jobline.part_type !== "PAE"
|
||||
? pol.jobline.part_type
|
||||
: null
|
||||
: responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
|
||||
: null
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receivebill")}
|
||||
</Button>
|
||||
</FeatureWrapperComponent>
|
||||
<PrintWrapper
|
||||
templateObject={{
|
||||
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
|
||||
variables: { id: record.id }
|
||||
}}
|
||||
messageObject={{
|
||||
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
|
||||
to: record.vendor.email
|
||||
}}
|
||||
id={job.id}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderListTableDrawerComponent);
|
||||
@@ -1,33 +1,23 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery, 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 React, { useEffect, useState } from "react";
|
||||
import { useMutation } from "@apollo/client";
|
||||
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 { 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 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 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,
|
||||
@@ -62,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 });
|
||||
|
||||
@@ -83,42 +60,17 @@ export function PartsOrderListTableComponent({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
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);
|
||||
}}
|
||||
>
|
||||
@@ -298,154 +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={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredPartsOrders = parts_orders
|
||||
? searchText === ""
|
||||
? parts_orders
|
||||
@@ -476,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={{
|
||||
|
||||
@@ -90,7 +90,7 @@ export function BillMarkSelectedExported({
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function PaymentMarkSelectedExported({
|
||||
onConfirm={handleUpdate}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
|
||||
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
import { Input, Space, Spin } from "antd";
|
||||
import React from "react";
|
||||
import { Button, Input, Space, Spin } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { ExclamationCircleFilled, ExclamationCircleOutlined } from "@ant-design/icons";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilters);
|
||||
|
||||
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
||||
const { t } = useTranslation();
|
||||
const [alertFilter, setAlertFilter] = useState(false);
|
||||
|
||||
const toggleAlertFilter = () => {
|
||||
const newAlertFilter = !alertFilter;
|
||||
setAlertFilter(newAlertFilter);
|
||||
setFilter({ ...filter, alert: newAlertFilter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
@@ -36,6 +45,13 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
|
||||
onChange={(emp) => setFilter({ ...filter, employeeId: emp })}
|
||||
allowClear
|
||||
/>
|
||||
<Button
|
||||
type={alertFilter ? "primary" : "default"}
|
||||
onClick={toggleAlertFilter}
|
||||
icon={alertFilter ? <ExclamationCircleFilled /> : <ExclamationCircleOutlined />}
|
||||
>
|
||||
{t("production.labels.alerts")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
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 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 }) => (
|
||||
<Tooltip title={title}>
|
||||
<div className="ellipses">{children}</div>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
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 && metadata.ins_co_nm) ||
|
||||
(cardSettings?.clm_no && metadata.clm_no) ||
|
||||
cardSettings?.employeeassignments ||
|
||||
(cardSettings?.actual_in && metadata.actual_in) ||
|
||||
(cardSettings?.scheduled_completion && metadata.scheduled_completion) ||
|
||||
(cardSettings?.ats && metadata.alt_transport) ||
|
||||
cardSettings?.sublets ||
|
||||
cardSettings?.production_note ||
|
||||
cardSettings?.partsstatus
|
||||
);
|
||||
}, [cardSettings, metadata]);
|
||||
|
||||
const headerContent = (
|
||||
<div className="header-content-container">
|
||||
<div className="inner-container">
|
||||
<ProductionAlert
|
||||
record={{
|
||||
id: card.id,
|
||||
production_vars: card?.metadata.production_vars,
|
||||
refetch: card?.refetch
|
||||
}}
|
||||
key="alert"
|
||||
/>
|
||||
{metadata?.suspended && <PauseCircleOutlined className="circle-outline" key="suspended" />}
|
||||
{metadata?.iouparent && (
|
||||
<EllipsesToolTip title={t("jobs.labels.iou")} key="iouparent" className="iouparent">
|
||||
<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>
|
||||
{cardSettings?.ownr_nm && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip title={<OwnerNameDisplay ownerObject={metadata} />}>
|
||||
{cardSettings.compact ? (
|
||||
`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`
|
||||
) : (
|
||||
<OwnerNameDisplay ownerObject={metadata} />
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{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 || ""}`}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.ins_co_nm && metadata.ins_co_nm && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.ins_co_nm || ""}>{metadata.ins_co_nm || ""}</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.clm_no && metadata.clm_no && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.clm_no || ""}>{metadata.clm_no || ""}</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
|
||||
>
|
||||
{`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`}
|
||||
>
|
||||
{`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
|
||||
>
|
||||
{`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`}
|
||||
>
|
||||
{`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.actual_in && metadata.actual_in && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.actual_in}>
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.scheduled_completion && metadata.scheduled_completion && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.scheduled_completion}>
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.ats && metadata.alt_transport && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip title={metadata.alt_transport}>{metadata.alt_transport || ""}</EllipsesToolTip>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.sublets && (
|
||||
<Col span={12}>
|
||||
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.production_note && (
|
||||
<Col span={24}>
|
||||
<ProductionListColumnProductionNote
|
||||
record={{
|
||||
production_vars: metadata?.production_vars,
|
||||
id: card?.id,
|
||||
refetch: card?.refetch
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings?.partsstatus && (
|
||||
<Col span={24}>
|
||||
<JobPartsQueueCount parts={metadata.joblines_status} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="react-trello-card"
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const CardColorLegend = ({ bodyshop }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Col style={{ marginLeft: "15px" }}>
|
||||
<Col>
|
||||
<Typography>{t("production.labels.legend")}</Typography>
|
||||
<List
|
||||
grid={{
|
||||
@@ -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,7 +1,7 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import Board from "./trello-board/index";
|
||||
import { Button, notification, Skeleton, Space, Statistic } from "antd";
|
||||
import { Button, notification, Skeleton, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -15,12 +15,14 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
|
||||
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
|
||||
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
|
||||
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 "./production-board-kanban.settings.component.jsx";
|
||||
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -39,7 +41,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
|
||||
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
|
||||
const [filter, setFilter] = useState({ search: "", employeeId: null });
|
||||
const [filter, setFilter] = useState(defaultFilters);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const [orientation, setOrientation] = useState("vertical");
|
||||
@@ -57,7 +59,12 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
|
||||
useEffect(() => {
|
||||
setIsMoving(true);
|
||||
const newBoardData = createBoardData(statuses, data, filter);
|
||||
const newBoardData = createBoardData({
|
||||
statuses,
|
||||
data,
|
||||
filter,
|
||||
cardSettings: associationSettings?.kanban_settings
|
||||
});
|
||||
|
||||
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
|
||||
...lane,
|
||||
@@ -72,7 +79,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
return prevBoardLanes;
|
||||
});
|
||||
setIsMoving(false);
|
||||
}, [data, bodyshop.md_ro_statuses, filter]);
|
||||
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
|
||||
|
||||
const getCardByID = useCallback((data, cardId) => {
|
||||
for (const lane of data.lanes) {
|
||||
@@ -149,8 +156,6 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
)
|
||||
});
|
||||
|
||||
// TODO (Note): This is causing the subscription to fire
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: draggableId,
|
||||
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
|
||||
@@ -174,58 +179,17 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
setIsMoving(false);
|
||||
}
|
||||
},
|
||||
[boardLanes, client, getCardByID, isMoving, t]
|
||||
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
|
||||
);
|
||||
|
||||
const totalHrs = useMemo(
|
||||
() =>
|
||||
data
|
||||
.reduce(
|
||||
(acc, val) =>
|
||||
acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1),
|
||||
[data]
|
||||
);
|
||||
const cardSettings = useMemo(() => {
|
||||
const kanbanSettings = associationSettings?.kanban_settings;
|
||||
return mergeWithDefaults(kanbanSettings);
|
||||
}, [associationSettings]);
|
||||
|
||||
const totalLAB = useMemo(
|
||||
() => data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1),
|
||||
[data]
|
||||
);
|
||||
|
||||
const totalLAR = useMemo(
|
||||
() => data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1),
|
||||
[data]
|
||||
);
|
||||
|
||||
const cardSettings = useMemo(
|
||||
() =>
|
||||
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,
|
||||
cardcolor: false,
|
||||
orientation: false,
|
||||
cardSize: "small",
|
||||
model_info: true
|
||||
},
|
||||
[associationSettings]
|
||||
);
|
||||
|
||||
const handleSettingsChange = useCallback((newSettings) => {
|
||||
setLoading(true);
|
||||
setOrientation(newSettings.orientation ? "vertical" : "horizontal");
|
||||
setLoading(false);
|
||||
}, []);
|
||||
const handleSettingsChange = () => {
|
||||
setFilter(defaultFilters);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton active />;
|
||||
@@ -235,14 +199,8 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
<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()}>
|
||||
@@ -253,13 +211,23 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
|
||||
parentLoading={setLoading}
|
||||
associationSettings={associationSettings}
|
||||
onSettingsChange={handleSettingsChange}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
|
||||
<NoteUpsertModal />
|
||||
<ProductionListDetailComponent jobs={data} />
|
||||
<Board data={boardLanes} onDragEnd={onDragEnd} orientation={orientation} cardSettings={cardSettings} />
|
||||
|
||||
<Board
|
||||
queryData={data}
|
||||
data={boardLanes}
|
||||
onDragEnd={onDragEnd}
|
||||
orientation={orientation}
|
||||
cardSettings={cardSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
QUERY_JOBS_IN_PRODUCTION_WITH_STATUSES,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION_WITH_STATUSES
|
||||
} 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";
|
||||
@@ -24,16 +21,14 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
|
||||
);
|
||||
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION_WITH_STATUSES, {
|
||||
variables: { statuses: combinedStatuses },
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||
pollInterval: 3600000,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION_WITH_STATUSES, {
|
||||
variables: { statuses: combinedStatuses },
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
@@ -42,6 +37,8 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
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}`));
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Col, Form, notification, Popover, Radio, Row } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
|
||||
export default function ProductionBoardKanbanSettings({ associationSettings, parentLoading }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (associationSettings?.kanban_settings) {
|
||||
form.setFieldsValue(associationSettings.kanban_settings);
|
||||
}
|
||||
}, [form, associationSettings]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
parentLoading(true);
|
||||
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: 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);
|
||||
parentLoading(false);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleValuesChange = () => setHasChanges(true);
|
||||
|
||||
const cardStyle = { minWidth: "50vw", marginTop: 10 };
|
||||
|
||||
const renderCheckboxItem = (name, labelKey) => (
|
||||
<Col span={4} key={name}>
|
||||
<Form.Item name={name} valuePropName="checked">
|
||||
<Checkbox>{t(labelKey)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const renderCardSettings = () => (
|
||||
<>
|
||||
<Card title={t("production.settings.layout")} style={cardStyle}>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={4}>
|
||||
<Form.Item name="orientation" label={t("production.labels.orientation")}>
|
||||
<Radio.Group>
|
||||
<Radio.Button value={true}>{t("production.labels.vertical")}</Radio.Button>
|
||||
<Radio.Button value={false}>{t("production.labels.horizontal")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item name="cardSize" label={t("production.labels.card_size")}>
|
||||
<Radio.Group>
|
||||
<Radio.Button value="small">{t("production.options.small")}</Radio.Button>
|
||||
<Radio.Button value="medium">{t("production.options.medium")}</Radio.Button>
|
||||
<Radio.Button value="large">{t("production.options.large")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item name="compact" label={t("production.labels.compact")}>
|
||||
<Radio.Group>
|
||||
<Radio.Button value={true}>{t("production.labels.tall")}</Radio.Button>
|
||||
<Radio.Button value={false}>{t("production.labels.wide")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Form.Item name="cardcolor" label={t("production.labels.cardcolor")}>
|
||||
<Radio.Group>
|
||||
<Radio.Button value={true}>{t("production.labels.on")}</Radio.Button>
|
||||
<Radio.Button value={false}>{t("production.labels.off")}</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Card title={t("production.settings.information")} style={cardStyle}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
"model_info",
|
||||
"ownr_nm",
|
||||
"clm_no",
|
||||
"ins_co_nm",
|
||||
"employeeassignments",
|
||||
"actual_in",
|
||||
"scheduled_completion",
|
||||
"ats",
|
||||
"production_note",
|
||||
"sublets",
|
||||
"partsstatus"
|
||||
].map((item) => renderCheckboxItem(item, `production.labels.${item}`))}
|
||||
</Row>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
|
||||
const overlay = (
|
||||
<Card>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||
{renderCardSettings()}
|
||||
<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={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(!open)}>
|
||||
{t("production.settings.board_settings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,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,6 +1,6 @@
|
||||
// Function to sort an array of objects by parentId
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
// Function to sort an array of objects by parentId
|
||||
const sortByParentId = (arr) => {
|
||||
let parentId = "-1";
|
||||
const sortedList = [];
|
||||
@@ -28,8 +28,8 @@ const sortByParentId = (arr) => {
|
||||
};
|
||||
|
||||
// Function to create board data based on statuses and jobs, with optional filtering
|
||||
export const createBoardData = (statuses, Jobs, filter) => {
|
||||
const { search, employeeId } = filter;
|
||||
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
||||
const { search, employeeId, alert } = filter;
|
||||
|
||||
const lanes = statuses.map((status) => ({
|
||||
id: status,
|
||||
@@ -37,8 +37,25 @@ export const createBoardData = (statuses, Jobs, filter) => {
|
||||
cards: []
|
||||
}));
|
||||
|
||||
const filteredJobs =
|
||||
(search === "" || !search) && !employeeId ? Jobs : Jobs.filter((job) => checkFilter(search, employeeId, job));
|
||||
let filteredJobs =
|
||||
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||
|
||||
// Filter jobs by selectedMdInsCos if it has values
|
||||
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
||||
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
||||
}
|
||||
|
||||
// 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}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter jobs by alert if alert filter is true
|
||||
if (alert) {
|
||||
filteredJobs = filteredJobs.filter((job) => job.production_vars?.alert);
|
||||
}
|
||||
|
||||
const DataGroupedByStatus = groupBy(filteredJobs, "status");
|
||||
|
||||
@@ -67,26 +84,24 @@ export const createBoardData = (statuses, Jobs, filter) => {
|
||||
|
||||
// Function to check if a job matches the search and/or employeeId filter
|
||||
const checkFilter = (search, employeeId, job) => {
|
||||
const lowerSearch = search?.toLowerCase() || "";
|
||||
const lowerSearch = search?.toLowerCase() ?? "";
|
||||
|
||||
const matchesSearch =
|
||||
lowerSearch &&
|
||||
((job.ro_number || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.ownr_fn || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.ownr_co_nm || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.ownr_ln || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.status || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.v_make_desc || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.v_model_desc || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.clm_no || "").toLowerCase().includes(lowerSearch) ||
|
||||
(job.plate_no || "").toLowerCase().includes(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 === employeeId ||
|
||||
job.employee_prep === employeeId ||
|
||||
job.employee_csr === employeeId ||
|
||||
job.employee_refinish === employeeId);
|
||||
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
|
||||
|
||||
return matchesSearch || matchesEmployeeId;
|
||||
};
|
||||
|
||||
@@ -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,69 @@
|
||||
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 defaultFilters = { search: "", employeeId: null, alert: false };
|
||||
|
||||
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, defaultFilters };
|
||||
@@ -0,0 +1,169 @@
|
||||
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";
|
||||
import { isFunction } from "lodash";
|
||||
|
||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data, onSettingsChange }) {
|
||||
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);
|
||||
|
||||
if (onSettingsChange && isFunction(onSettingsChange)) {
|
||||
onSettingsChange(values);
|
||||
}
|
||||
|
||||
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,
|
||||
onSettingsChange: PropTypes.func,
|
||||
data: PropTypes.array
|
||||
};
|
||||
|
||||
export default ProductionBoardKanbanSettings;
|
||||
@@ -14,6 +14,7 @@ const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, item
|
||||
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;
|
||||
@@ -31,16 +32,30 @@ const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, item
|
||||
};
|
||||
|
||||
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]);
|
||||
}, [itemKey, setMaxHeight, devicePixelRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemKey && heightMapRef.current.has(itemKey)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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());
|
||||
@@ -30,7 +31,8 @@ const BoardContainer = ({
|
||||
orientation = "horizontal",
|
||||
cardSettings = {},
|
||||
eventBusHandle,
|
||||
reducerData
|
||||
reducerData,
|
||||
queryData
|
||||
}) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
@@ -124,33 +126,36 @@ const BoardContainer = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EyeInvisibleOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../../../redux/user/user.selectors.js";
|
||||
import { selectTechnician } from "../../../../redux/tech/tech.selectors.js";
|
||||
import ProductionBoardCard from "../../../production-board-kanban-card/production-board-kanban-card.component.jsx";
|
||||
import 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";
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import { Button, Dropdown } from "antd";
|
||||
import dataSource from "./production-list-columns.data";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
@@ -10,16 +9,23 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
technician: selectTechnician,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionColumnsComponent);
|
||||
|
||||
export function ProductionColumnsComponent({ columnState, technician, bodyshop, data, tableState, refetch }) {
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
// Add any necessary dispatch actions here
|
||||
});
|
||||
|
||||
export function ProductionColumnsComponent({
|
||||
columnState,
|
||||
technician,
|
||||
bodyshop,
|
||||
data,
|
||||
tableState,
|
||||
refetch,
|
||||
onColumnAdd
|
||||
}) {
|
||||
const [columns, setColumns] = columnState;
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -29,18 +35,26 @@ export function ProductionColumnsComponent({ columnState, technician, bodyshop,
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
const handleAdd = (e) => {
|
||||
setColumns([
|
||||
...columns,
|
||||
...dataSource({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: tableState,
|
||||
data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Enhanced_Payroll }
|
||||
}).filter((i) => i.key === e.key)
|
||||
]);
|
||||
const newColumn = dataSource({
|
||||
bodyshop,
|
||||
technician,
|
||||
state: tableState,
|
||||
data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Enhanced_Payroll }
|
||||
}).find((i) => i.key === e.key);
|
||||
|
||||
if (newColumn) {
|
||||
const updatedColumns = [...columns, newColumn];
|
||||
setColumns(updatedColumns);
|
||||
|
||||
// Call the onColumnAdd function passed as a prop
|
||||
if (onColumnAdd) {
|
||||
onColumnAdd(newColumn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columnKeys = columns.map((i) => i.key);
|
||||
@@ -76,12 +90,4 @@ export function ProductionColumnsComponent({ columnState, technician, bodyshop,
|
||||
);
|
||||
}
|
||||
|
||||
// <Transfer
|
||||
// dataSource={dataSource}
|
||||
// titles={["Source", "Target"]}
|
||||
// targetKeys={columns.map((c) => c.key)}
|
||||
// render={(item) => item.title}
|
||||
// onChange={(nextTargetKeys, direction, moveKeys) => {
|
||||
// setColumns(dataSource.filter((i) => nextTargetKeys.includes(i.key)));
|
||||
// }}
|
||||
// />
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionColumnsComponent);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExclamationCircleFilled } from "@ant-design/icons";
|
||||
import { ExclamationCircleFilled, PlusCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button } from "antd";
|
||||
import { Button, Popconfirm } from "antd";
|
||||
import React, { useCallback } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -8,6 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
@@ -22,22 +23,24 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
)
|
||||
});
|
||||
|
||||
const ProductionListColumnAlert = ({ record, insertAuditTrail }) => {
|
||||
const ProductionListColumnAlert = ({ id, productionVars, refetch, insertAuditTrail }) => {
|
||||
const [updateAlert] = useMutation(UPDATE_JOB);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleAlertToggle = useCallback(() => {
|
||||
logImEXEvent("production_toggle_alert");
|
||||
|
||||
const newAlertState = !!record.production_vars?.alert ? !record.production_vars.alert : true;
|
||||
const newAlertState = !!productionVars?.alert ? !productionVars?.alert : true;
|
||||
const finalProductionVars = {
|
||||
...productionVars,
|
||||
alert: newAlertState
|
||||
};
|
||||
|
||||
updateAlert({
|
||||
variables: {
|
||||
jobId: record.id,
|
||||
jobId: id,
|
||||
job: {
|
||||
production_vars: {
|
||||
...record.production_vars,
|
||||
alert: newAlertState
|
||||
}
|
||||
production_vars: finalProductionVars
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
@@ -45,17 +48,26 @@ const ProductionListColumnAlert = ({ record, insertAuditTrail }) => {
|
||||
});
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: record.id,
|
||||
jobid: id,
|
||||
operation: AuditTrailMapping.alertToggle(newAlertState),
|
||||
type: "alertToggle"
|
||||
});
|
||||
|
||||
if (record.refetch) record.refetch();
|
||||
}, [updateAlert, insertAuditTrail, record]);
|
||||
if (refetch) refetch();
|
||||
}, [updateAlert, insertAuditTrail, id, productionVars, refetch]);
|
||||
|
||||
if (!record.production_vars?.alert) return null;
|
||||
|
||||
return <Button className="production-alert" icon={<ExclamationCircleFilled />} onClick={handleAlertToggle} />;
|
||||
return productionVars?.alert ? (
|
||||
<Popconfirm
|
||||
title={t("general.actions.remove_alert")}
|
||||
onConfirm={handleAlertToggle}
|
||||
okText={t("general.labels.yes")}
|
||||
cancelText={t("general.labels.no")}
|
||||
>
|
||||
<Button className="production-alert" icon={<ExclamationCircleFilled />} />
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Button className="muted-button" icon={<PlusCircleFilled />} onClick={handleAlertToggle} />
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListColumnAlert);
|
||||
|
||||
@@ -2,10 +2,13 @@ import { BranchesOutlined, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Checkbox, Space, Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { store } from "../../redux/store";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { TimeFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
@@ -23,10 +26,7 @@ import ProductionListColumnPartsReceived from "./production-list-columns.partsre
|
||||
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
|
||||
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||
import { store } from "../../redux/store";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||
|
||||
const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
|
||||
const { Enhanced_Payroll } = treatments;
|
||||
@@ -258,7 +258,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false }
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.special_coverage_policy),
|
||||
onFilter: (value, record) => value === record.special_coverage_policy,
|
||||
render: (text, record) => <Checkbox checked={record.special_coverage_policy} />
|
||||
},
|
||||
|
||||
@@ -349,7 +349,14 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
||||
key: "alert",
|
||||
sorter: (a, b) => Number(a.production_vars?.alert || false) - Number(b.production_vars?.alert || false),
|
||||
sortOrder: state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
|
||||
render: (text, record) => <ProductionListColumnAlert record={{ record }} />
|
||||
filters: [
|
||||
{ text: "True", value: true },
|
||||
{ text: "False", value: false }
|
||||
],
|
||||
onFilter: (value, record) => value === (record.production_vars?.alert || false),
|
||||
render: (text, record) => (
|
||||
<ProductionListColumnAlert id={record.id} productionVars={record?.production_vars} refetch={refetch} />
|
||||
)
|
||||
},
|
||||
{
|
||||
title: i18n.t("production.labels.note"),
|
||||
@@ -370,7 +377,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
|
||||
dataIndex: "tt",
|
||||
key: "tt",
|
||||
render: (text, record) => {
|
||||
return <ProductionlistColumnTouchTime job={record} />;
|
||||
return <ProductionListColumnTouchTime job={record} />;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Dropdown, TimePicker } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import { Button, Card, Dropdown, Space, TimePicker } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
|
||||
export default function ProductionListDate({ record, field, time, pastIndicator }) {
|
||||
@@ -56,23 +56,25 @@ export default function ProductionListDate({ record, field, time, pastIndicator
|
||||
key: "overlayItem1",
|
||||
label: (
|
||||
<Card style={{ padding: "1rem" }} onClick={(e) => e.stopPropagation()}>
|
||||
<FormDatePicker
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
value={(record[field] && dayjs(record[field])) || null}
|
||||
onChange={handleChange}
|
||||
format="MM/DD/YYYY"
|
||||
isDateOnly={!time}
|
||||
/>
|
||||
{time && (
|
||||
<TimePicker
|
||||
<Space direction={"vertical"}>
|
||||
<FormDatePicker
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
value={(record[field] && dayjs(record[field])) || null}
|
||||
onChange={handleChange}
|
||||
minuteStep={15}
|
||||
format="hh:mm a"
|
||||
format="MM/DD/YYYY"
|
||||
isDateOnly={!time}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
|
||||
{time && (
|
||||
<TimePicker
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
value={(record[field] && dayjs(record[field])) || null}
|
||||
onChange={handleChange}
|
||||
minuteStep={15}
|
||||
format="hh:mm a"
|
||||
/>
|
||||
)}
|
||||
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
|
||||
) : (
|
||||
<PlusCircleFilled
|
||||
style={iconStyle}
|
||||
className="muted-button"
|
||||
onClick={() => {
|
||||
setAssignment({ operation: type });
|
||||
setVisibility(true);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button, Form, Input, notification, Popover, Space } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { isFunction } from "lodash";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -16,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function ProductionListSaveConfigButton({ columns, bodyshop, tableState }) {
|
||||
export function ProductionListSaveConfigButton({ columns, bodyshop, tableState, onSave }) {
|
||||
const [updateShop] = useMutation(UPDATE_SHOP);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -49,6 +50,9 @@ export function ProductionListSaveConfigButton({ columns, bodyshop, tableState }
|
||||
});
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bodyshop.successes.save") });
|
||||
if (onSave && isFunction(onSave)) {
|
||||
onSave();
|
||||
}
|
||||
} else {
|
||||
notification["error"]({
|
||||
message: t("bodyshop.errors.saving", {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { isFunction } from "lodash";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -18,7 +19,17 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
export function ProductionListTable({ refetch, bodyshop, technician, currentUser, state, data, setColumns, setState }) {
|
||||
export function ProductionListTable({
|
||||
refetch,
|
||||
bodyshop,
|
||||
technician,
|
||||
currentUser,
|
||||
state,
|
||||
data,
|
||||
setColumns,
|
||||
setState,
|
||||
onProfileChange
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
|
||||
const [updateShop] = useMutation(UPDATE_SHOP);
|
||||
@@ -32,25 +43,25 @@ export function ProductionListTable({ refetch, bodyshop, technician, currentUser
|
||||
});
|
||||
|
||||
const handleSelect = async (value, option) => {
|
||||
setColumns(
|
||||
bodyshop.production_config
|
||||
.filter((pc) => pc.name === value)[0]
|
||||
.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Enhanced_Payroll }
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width
|
||||
};
|
||||
})
|
||||
);
|
||||
setState(bodyshop.production_config.filter((pc) => pc.name === value)[0].columns.tableState);
|
||||
const newColumns = bodyshop.production_config
|
||||
.filter((pc) => pc.name === value)[0]
|
||||
.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Enhanced_Payroll }
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width
|
||||
};
|
||||
});
|
||||
setColumns(newColumns);
|
||||
const newState = bodyshop.production_config.filter((pc) => pc.name === value)[0].columns.tableState;
|
||||
setState(newState);
|
||||
|
||||
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
||||
|
||||
@@ -72,6 +83,10 @@ export function ProductionListTable({ refetch, bodyshop, technician, currentUser
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (onProfileChange && isFunction(onProfileChange)) {
|
||||
onProfileChange({ value, option, newColumns, newState, assoc });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrash = async (name) => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import ReactDragListView from "react-drag-listview";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -16,6 +14,11 @@ import ProductionListSaveConfigButton from "../production-list-save-config-butto
|
||||
import ProductionListPrint from "./production-list-print.component";
|
||||
import ProductionListTableViewSelect from "./production-list-table-view-select.component";
|
||||
import ResizeableTitle from "./production-list-table.resizeable.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import Prompt from "../../utils/prompt.js";
|
||||
import _ from "lodash";
|
||||
import AlertComponent from "../alert/alert.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -25,6 +28,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function ProductionListTable({ loading, data, refetch, bodyshop, technician, currentUser }) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||
|
||||
const {
|
||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||
@@ -35,10 +39,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
});
|
||||
|
||||
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
|
||||
|
||||
const defaultView = assoc && assoc.default_prod_list_view;
|
||||
|
||||
const [state, setState] = useState(
|
||||
const initialStateRef = useRef(
|
||||
(bodyshop.production_config &&
|
||||
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
|
||||
bodyshop.production_config[0]?.columns.tableState || {
|
||||
@@ -47,80 +50,102 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
}
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const matchingColumnConfig = useMemo(() => {
|
||||
return bodyshop.production_config.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config, defaultView]);
|
||||
|
||||
const [columns, setColumns] = useState(
|
||||
(state &&
|
||||
matchingColumnConfig &&
|
||||
matchingColumnConfig.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
state,
|
||||
data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width ?? 100
|
||||
};
|
||||
})) ||
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns =
|
||||
(state &&
|
||||
matchingColumnConfig &&
|
||||
matchingColumnConfig.columns.columnKeys.map((k) => {
|
||||
const initialColumnsRef = useRef(
|
||||
(initialStateRef.current &&
|
||||
bodyshop.production_config
|
||||
.find((p) => p.name === defaultView)
|
||||
?.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
refetch,
|
||||
state,
|
||||
data: data,
|
||||
technician,
|
||||
state: initialStateRef.current,
|
||||
data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width ?? 100
|
||||
};
|
||||
})) ||
|
||||
[];
|
||||
setColumns(newColumns);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const [state, setState] = useState(initialStateRef.current);
|
||||
const [columns, setColumns] = useState(initialColumnsRef.current);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const matchingColumnConfig = useMemo(() => {
|
||||
return bodyshop.production_config.find((p) => p.name === defaultView);
|
||||
}, [bodyshop.production_config, defaultView]);
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns =
|
||||
matchingColumnConfig?.columns.columnKeys.map((k) => {
|
||||
return {
|
||||
...ProductionListColumns({
|
||||
bodyshop,
|
||||
technician,
|
||||
refetch,
|
||||
state,
|
||||
data: data,
|
||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||
treatments: { Production_List_Status_Colors, Enhanced_Payroll }
|
||||
}).find((e) => e.key === k.key),
|
||||
width: k.width ?? 100
|
||||
};
|
||||
}) || [];
|
||||
|
||||
// Only update columns if they haven't been manually changed by the user
|
||||
if (_.isEqual(initialColumnsRef.current, columns)) {
|
||||
setColumns(newColumns);
|
||||
}
|
||||
}, [
|
||||
//state,
|
||||
matchingColumnConfig,
|
||||
bodyshop,
|
||||
technician,
|
||||
data
|
||||
]); //State removed from dependency array as it causes race condition when removing columns from table view and is not needed.
|
||||
data,
|
||||
Enhanced_Payroll,
|
||||
Production_List_Status_Colors,
|
||||
refetch,
|
||||
state,
|
||||
columns
|
||||
]);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({
|
||||
const newState = {
|
||||
...state,
|
||||
filteredInfo: filters,
|
||||
sortedInfo: { columnKey: sorter.columnKey, order: sorter.order }
|
||||
});
|
||||
};
|
||||
if (!_.isEqual(newState, state)) {
|
||||
setState(newState);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnd = (fromIndex, toIndex) => {
|
||||
const columnsCopy = columns.slice();
|
||||
const item = columnsCopy.splice(fromIndex, 1)[0];
|
||||
columnsCopy.splice(toIndex, 0, item);
|
||||
setColumns(columnsCopy);
|
||||
if (fromIndex === toIndex) return;
|
||||
|
||||
const columnsCopy = [...columns];
|
||||
const [movedItem] = columnsCopy.splice(fromIndex, 1);
|
||||
columnsCopy.splice(toIndex, 0, movedItem);
|
||||
|
||||
if (!_.isEqual(columnsCopy, columns)) {
|
||||
setColumns(columnsCopy);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const removeColumn = (e) => {
|
||||
const { key } = e;
|
||||
const newColumns = columns.filter((i) => i.key !== key);
|
||||
setColumns(newColumns);
|
||||
|
||||
if (!_.isEqual(newColumns, columns)) {
|
||||
setColumns(newColumns);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize =
|
||||
@@ -131,9 +156,21 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
...nextColumns[index],
|
||||
width: size.width
|
||||
};
|
||||
setColumns(nextColumns);
|
||||
|
||||
if (!_.isEqual(nextColumns, columns)) {
|
||||
setColumns(nextColumns);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const addColumn = (newColumn) => {
|
||||
const updatedColumns = [...columns, newColumn];
|
||||
if (!_.isEqual(updatedColumns, columns)) {
|
||||
setColumns(updatedColumns);
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
};
|
||||
|
||||
const headerItem = (col) => {
|
||||
const menu = {
|
||||
onClick: removeColumn,
|
||||
@@ -152,29 +189,29 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
);
|
||||
};
|
||||
|
||||
const dataSource =
|
||||
searchText === ""
|
||||
? data
|
||||
: data.filter(
|
||||
(j) =>
|
||||
(j.ro_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.ownr_co_nm || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.ownr_fn || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.ownr_ln || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.status || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.ins_co_nm || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.v_model_desc || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
(j.v_make_desc || "").toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
const resetChanges = () => {
|
||||
setState(initialStateRef.current);
|
||||
setColumns(initialColumnsRef.current);
|
||||
setHasUnsavedChanges(false);
|
||||
};
|
||||
|
||||
// const handleSelectRecord = (record) => {
|
||||
// if (selected !== record.id) {
|
||||
// setSelected(record.id);
|
||||
// } else {
|
||||
// setSelected(null);
|
||||
// }
|
||||
// };
|
||||
const filterData = (item, searchText) => {
|
||||
const fieldsToSearch = [
|
||||
item.ro_number,
|
||||
item.ownr_co_nm,
|
||||
item.ownr_fn,
|
||||
item.ownr_ln,
|
||||
item.status,
|
||||
item.ins_co_nm,
|
||||
item.clm_no,
|
||||
item.v_model_desc,
|
||||
item.v_make_desc
|
||||
];
|
||||
|
||||
return fieldsToSearch.some((field) => (field || "").toString().toLowerCase().includes(searchText.toLowerCase()));
|
||||
};
|
||||
|
||||
const dataSource = searchText === "" ? data : data.filter((j) => filterData(j, searchText));
|
||||
|
||||
if (!!!columns) return <div>No columns found.</div>;
|
||||
|
||||
@@ -186,8 +223,29 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
.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);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Prompt when={hasUnsavedChanges} beforeUnload={true} message={t("general.messages.unsavedchangespopup")} />
|
||||
{hasUnsavedChanges && (
|
||||
<AlertComponent
|
||||
type="warning"
|
||||
message={
|
||||
<div>
|
||||
<span>{t("general.messages.unsavedchanges")} </span>
|
||||
<span
|
||||
onClick={resetChanges}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline"
|
||||
}}
|
||||
>
|
||||
{t("general.actions.reset")}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<PageHeader
|
||||
title={
|
||||
<Space>
|
||||
@@ -199,20 +257,37 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
|
||||
}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch && refetch()}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch && refetch();
|
||||
}}
|
||||
>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<ProductionListColumnsAdd columnState={[columns, setColumns]} tableState={state} data={data} />
|
||||
<ProductionListSaveConfigButton columns={columns} tableState={state} />
|
||||
|
||||
<ProductionListColumnsAdd
|
||||
columnState={[columns, setColumns]}
|
||||
tableState={state}
|
||||
data={data}
|
||||
onColumnAdd={addColumn}
|
||||
/>
|
||||
<ProductionListSaveConfigButton
|
||||
columns={columns}
|
||||
tableState={state}
|
||||
onSave={() => {
|
||||
setHasUnsavedChanges(false);
|
||||
}}
|
||||
/>
|
||||
<ProductionListTableViewSelect
|
||||
state={state}
|
||||
setState={setState}
|
||||
setColumns={setColumns}
|
||||
onProfileChange={() => {
|
||||
initialStateRef.current = state;
|
||||
setHasUnsavedChanges(false);
|
||||
}}
|
||||
refetch={refetch}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<Input
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -51,6 +52,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
<Collapse>
|
||||
<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={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_tax_in"]}
|
||||
@@ -58,6 +65,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAB", "lbr_tx_in1"]}
|
||||
@@ -95,6 +120,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAD")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_tax_in"]}
|
||||
@@ -102,6 +133,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_tx_in1"]}
|
||||
@@ -139,6 +188,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAE")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_tax_in"]}
|
||||
@@ -146,6 +201,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_tx_in1"]}
|
||||
@@ -183,6 +256,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAF")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_tax_in"]}
|
||||
@@ -190,6 +269,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_tx_in1"]}
|
||||
@@ -227,6 +324,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAG")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_tax_in"]}
|
||||
@@ -234,6 +337,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_tx_in1"]}
|
||||
@@ -271,6 +392,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAM")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_tax_in"]}
|
||||
@@ -278,6 +405,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_tx_in1"]}
|
||||
@@ -315,6 +460,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAR")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_tax_in"]}
|
||||
@@ -322,6 +473,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_tx_in1"]}
|
||||
@@ -359,6 +528,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAS")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_tax_in"]}
|
||||
@@ -366,6 +541,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_tx_in1"]}
|
||||
@@ -403,6 +596,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAU")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAU", "lbr_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAU", "lbr_tax_in"]}
|
||||
@@ -410,6 +609,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAU", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfl", "LAU", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={2} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfl", "LAU", "lbr_tx_in1"]}
|
||||
@@ -462,7 +679,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "tax_ind"]}
|
||||
@@ -470,6 +692,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfm", "MAPA", "tax_ind"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_tx_in1"]}
|
||||
@@ -519,7 +759,12 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_adjp")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]}
|
||||
>
|
||||
<InputNumber min={-100} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MASH", "tax_ind"]}
|
||||
@@ -527,6 +772,24 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_taxp")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["md_responsibility_centers", "cieca_pfm", "MASH", "tax_ind"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_tx_in1"]}
|
||||
@@ -1765,25 +2028,37 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("jobs.fields.tax_tow_rt")} name="tax_tow_rt">
|
||||
<Form.Item label={t("jobs.fields.tax_tow_rt")} name={["md_responsibility_centers", "tax_tow_rt"]}>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
|
||||
<Form.Item label={t("jobs.fields.tax_str_rt")} name={["md_responsibility_centers", "tax_str_rt"]}>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.tax_paint_mat_rt")}
|
||||
name={["md_responsibility_centers", "tax_paint_mat_rt"]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.tax_shop_mat_rt")}
|
||||
name={["md_responsibility_centers", "tax_shop_mat_rt"]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : null}
|
||||
<Form.Item label={t("jobs.fields.tax_sub_rt")} name={["md_responsibility_centers", "tax_sub_rt"]}>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_rt">
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name={["md_responsibility_centers", "tax_lbr_rt"]}>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item label={t("jobs.fields.tax_levies_rt")} name={["md_responsibility_centers", "tax_levies_rt"]}>
|
||||
<InputNumber min={0} max={100} precision={4} />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AlertOutlined } from "@ant-design/icons";
|
||||
import { Alert, Button, Col, Row, Space } from "antd";
|
||||
import i18n from "i18next";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -12,39 +12,42 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
updateAvailable: selectUpdateAvailable
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
// setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UpdateAlert);
|
||||
const intervalMS = 10 * 60 * 1000;
|
||||
|
||||
export function UpdateAlert({ updateAvailable }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
offlineReady: [
|
||||
offlineReady //setOfflineReady
|
||||
],
|
||||
needRefresh: [
|
||||
needRefresh //setNeedRefresh
|
||||
],
|
||||
offlineReady: [offlineReady],
|
||||
needRefresh: [needRefresh],
|
||||
updateServiceWorker
|
||||
} = useRegisterSW({
|
||||
onRegistered(r) {
|
||||
// eslint-disable-next-line prefer-template
|
||||
console.log("SW Registered: " + r);
|
||||
r &&
|
||||
setInterval(() => {
|
||||
r.update();
|
||||
}, intervalMS);
|
||||
console.log("SW Registered:", r);
|
||||
if (r) {
|
||||
setInterval(
|
||||
() => {
|
||||
r.update();
|
||||
},
|
||||
10 * 60 * 1000
|
||||
);
|
||||
}
|
||||
},
|
||||
onRegisterError(error) {
|
||||
console.log("SW registration error", error);
|
||||
console.error("SW registration error", error);
|
||||
}
|
||||
});
|
||||
|
||||
if (import.meta.env.DEV) console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
|
||||
}
|
||||
}, [needRefresh, offlineReady]);
|
||||
|
||||
if (!needRefresh) return null;
|
||||
|
||||
return (
|
||||
<Alert
|
||||
message={t("general.messages.newversiontitle", {
|
||||
@@ -69,19 +72,10 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
</Col>
|
||||
<Col sm={24} md={8} lg={6}>
|
||||
<Space wrap>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
window.open("https://imex-online.noticeable.news/", "_blank");
|
||||
}}
|
||||
>
|
||||
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
|
||||
{i18n.t("general.actions.viewreleasenotes")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
updateServiceWorker(true);
|
||||
}}
|
||||
>
|
||||
<Button type="primary" onClick={() => updateServiceWorker(true)}>
|
||||
{i18n.t("general.actions.refresh")}
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -93,3 +87,5 @@ export function UpdateAlert({ updateAvailable }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UpdateAlert);
|
||||
|
||||
@@ -50,7 +50,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_BILLS_BY_JOBID = gql`
|
||||
export const QUERY_PARTS_BILLS_BY_JOBID = gql`
|
||||
query QUERY_PARTS_BILLS_BY_JOBID($jobid: uuid!) {
|
||||
parts_orders(where: { jobid: { _eq: $jobid } }, order_by: { order_date: desc }) {
|
||||
id
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql`
|
||||
query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED(
|
||||
query QUERY_ALL_ACTIVE_JOBS_PAGINATED(
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [jobs_order_by!]
|
||||
@@ -292,82 +292,6 @@ export const QUERY_EXACT_JOBS_IN_PRODUCTION = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
query QUERY_JOBS_IN_PRODUCTION {
|
||||
jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
updated_at
|
||||
comment
|
||||
status
|
||||
category
|
||||
iouparent
|
||||
ro_number
|
||||
ownerid
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
clm_no
|
||||
v_make_desc
|
||||
v_color
|
||||
vehicleid
|
||||
plate_no
|
||||
actual_in
|
||||
scheduled_completion
|
||||
scheduled_delivery
|
||||
date_last_contacted
|
||||
date_next_contact
|
||||
ins_co_nm
|
||||
clm_total
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
special_coverage_policy
|
||||
owner_owing
|
||||
production_vars
|
||||
kanbanparent
|
||||
alt_transport
|
||||
employee_body
|
||||
employee_refinish
|
||||
employee_prep
|
||||
employee_csr
|
||||
est_ct_fn
|
||||
est_ct_ln
|
||||
suspended
|
||||
date_repairstarted
|
||||
joblines_status {
|
||||
part_type
|
||||
status
|
||||
count
|
||||
}
|
||||
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _eq: "LAR" } }, { removed: { _eq: false } }] }) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
subletLines: joblines(
|
||||
where: { _and: { part_type: { _in: ["PAS", "PASL"] }, removed: { _eq: false } } }
|
||||
order_by: { line_no: asc }
|
||||
) {
|
||||
id
|
||||
line_desc
|
||||
sublet_ignored
|
||||
sublet_completed
|
||||
jobid
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_LBR_HRS_BY_PK = gql`
|
||||
query QUERY_LBR_HRS_BY_PK($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
@@ -1138,6 +1062,7 @@ export const UPDATE_JOB = gql`
|
||||
suspended
|
||||
queued_for_parts
|
||||
scheduled_completion
|
||||
scheduled_delivery
|
||||
actual_in
|
||||
date_repairstarted
|
||||
date_void
|
||||
@@ -2528,9 +2453,23 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOBS_IN_PRODUCTION_WITH_STATUSES = gql`
|
||||
query QUERY_JOBS_IN_PRODUCTION($statuses: [String!]) {
|
||||
jobs(where: { inproduction: { _eq: true }, status: { _in: $statuses } }) {
|
||||
export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
|
||||
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION {
|
||||
jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOBS_IN_PRODUCTION = gql`
|
||||
query QUERY_JOBS_IN_PRODUCTION {
|
||||
jobs(where: { inproduction: { _eq: true } }) {
|
||||
tasks_aggregate(where: { completed: { _eq: false }, deleted: { _eq: false } }) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
id
|
||||
updated_at
|
||||
comment
|
||||
@@ -2570,6 +2509,7 @@ export const QUERY_JOBS_IN_PRODUCTION_WITH_STATUSES = gql`
|
||||
est_ct_fn
|
||||
est_ct_ln
|
||||
suspended
|
||||
job_totals
|
||||
date_repairstarted
|
||||
joblines_status {
|
||||
part_type
|
||||
@@ -2603,21 +2543,3 @@ export const QUERY_JOBS_IN_PRODUCTION_WITH_STATUSES = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SUBSCRIPTION_JOBS_IN_PRODUCTION_WITH_STATUSES = gql`
|
||||
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION($statuses: [String!]) {
|
||||
jobs(where: { inproduction: { _eq: true }, status: { _in: $statuses } }) {
|
||||
id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
|
||||
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION {
|
||||
jobs(where: { inproduction: { _eq: true } }) {
|
||||
id
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||
import { QUERY_OWNER_FOR_JOB_CREATION } from "../../graphql/owners.queries";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsCreateComponent from "./jobs-create.component";
|
||||
import JobCreateContext from "./jobs-create.context";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -159,13 +159,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
layout="vertical"
|
||||
autoComplete={"off"}
|
||||
initialValues={{
|
||||
tax_tow_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_str_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_paint_mat_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_shop_mat_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_sub_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_lbr_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_levies_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate / 100,
|
||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate / 100,
|
||||
@@ -261,19 +254,34 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
}
|
||||
},
|
||||
rome: {
|
||||
cieca_pft: {
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty1,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty2,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty3,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty4,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty5
|
||||
},
|
||||
materials: bodyshop.md_responsibility_centers.cieca_pfm,
|
||||
cieca_pfl: bodyshop.md_responsibility_centers.cieca_pfl,
|
||||
parts_tax_rates: bodyshop.md_responsibility_centers.parts_tax_rates
|
||||
}
|
||||
}
|
||||
tax_tow_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_str_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_paint_mat_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_shop_mat_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_sub_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_lbr_rt: bodyshop.bill_tax_rates.state_tax_rate / 100,
|
||||
tax_levies_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
|
||||
},
|
||||
rome: {
|
||||
cieca_pft: {
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty1,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty2,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty3,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty4,
|
||||
...bodyshop.md_responsibility_centers.taxes.tax_ty5
|
||||
},
|
||||
materials: bodyshop.md_responsibility_centers.cieca_pfm,
|
||||
cieca_pfl: bodyshop.md_responsibility_centers.cieca_pfl,
|
||||
parts_tax_rates: bodyshop.md_responsibility_centers.parts_tax_rates,
|
||||
tax_tow_rt: bodyshop.md_responsibility_centers.tax_tow_rt,
|
||||
tax_str_rt: bodyshop.md_responsibility_centers.tax_str_rt,
|
||||
tax_paint_mat_rt: bodyshop.md_responsibility_centers.tax_paint_mat_rt,
|
||||
tax_shop_mat_rt: bodyshop.md_responsibility_centers.tax_shop_mat_rt,
|
||||
tax_sub_rt: bodyshop.md_responsibility_centers.tax_sub_rt,
|
||||
tax_lbr_rt: bodyshop.md_responsibility_centers.tax_lbr_rt,
|
||||
tax_levies_rt: bodyshop.md_responsibility_centers.tax_levies_rt
|
||||
},
|
||||
promanager: "USE_ROME"
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -8,11 +8,11 @@ import Icon, {
|
||||
SyncOutlined,
|
||||
ToolFilled
|
||||
} from "@ant-design/icons";
|
||||
import { Badge, Button, Divider, Form, notification, Space, Tabs } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Badge, Button, Divider, Form, notification, Space, Tabs } from "antd";
|
||||
import Axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -20,11 +20,13 @@ import { FaHardHat, FaRegStickyNote, FaShieldAlt, FaTasks } from "react-icons/fa
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
|
||||
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
|
||||
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
|
||||
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
|
||||
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
|
||||
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
|
||||
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component";
|
||||
@@ -42,19 +44,18 @@ import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gal
|
||||
import JobNotesContainer from "../../components/jobs-notes/jobs-notes.container";
|
||||
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
import ScheduleJobModalContainer from "../../components/schedule-job-modal/schedule-job-modal.container";
|
||||
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
||||
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries.js";
|
||||
import { QUERY_JOB_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import _ from "lodash";
|
||||
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import TaskListContainer from "../../components/task-list/task-list.container.jsx";
|
||||
import { QUERY_JOB_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -95,6 +96,11 @@ export function JobsDetailPage({
|
||||
const formItemLayout = {
|
||||
layout: "vertical"
|
||||
};
|
||||
const billsQuery = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
//form.setFieldsValue(transormJobToForm(job));
|
||||
@@ -103,6 +109,42 @@ export function JobsDetailPage({
|
||||
|
||||
//useKeyboardSaveShortcut(form.submit);
|
||||
|
||||
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) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -302,7 +344,18 @@ export function JobsDetailPage({
|
||||
id: "job-details-repairdata",
|
||||
label: t("menus.jobsdetail.repairdata"),
|
||||
forceRender: true,
|
||||
children: <JobsLinesContainer job={job} joblines={job.joblines} refetch={refetch} form={form} />
|
||||
children: (
|
||||
<JobsLinesContainer
|
||||
job={job}
|
||||
joblines={job.joblines}
|
||||
billsQuery={billsQuery}
|
||||
handleBillOnRowClick={handleBillOnRowClick}
|
||||
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
||||
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
|
||||
refetch={refetch}
|
||||
form={form}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "rates",
|
||||
@@ -326,7 +379,15 @@ export function JobsDetailPage({
|
||||
label: HasFeatureAccess({ featureName: "bills", bodyshop })
|
||||
? t("menus.jobsdetail.partssublet")
|
||||
: t("menus.jobsdetail.parts"),
|
||||
children: <JobsDetailPliContainer job={job} />
|
||||
children: (
|
||||
<JobsDetailPliContainer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleBillOnRowClick={handleBillOnRowClick}
|
||||
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
||||
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
|
||||
/>
|
||||
)
|
||||
},
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
|
||||
@@ -12,7 +12,6 @@ import ErrorBoundary from "../../components/error-boundary/error-boundary.compon
|
||||
//import FooterComponent from "../../components/footer/footer.component";
|
||||
//Component Imports
|
||||
import * as Sentry from "@sentry/react";
|
||||
import Joyride from "react-joyride";
|
||||
import TestComponent from "../../components/_test/test.page";
|
||||
import HeaderContainer from "../../components/header/header.container";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
@@ -23,8 +22,6 @@ import { requestForToken } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
|
||||
|
||||
import UpdateAlert from "../../components/update-alert/update-alert.component";
|
||||
import { setJoyRideFinished } from "../../redux/application/application.actions.js";
|
||||
import { selectEnableJoyRide, selectJoyRideSteps } from "../../redux/application/application.selectors.js";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
|
||||
import "./manage.page.styles.scss";
|
||||
|
||||
@@ -105,16 +102,12 @@ const { Content, Footer } = Layout;
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
conflict: selectInstanceConflict,
|
||||
bodyshop: selectBodyshop,
|
||||
enableJoyRide: selectEnableJoyRide,
|
||||
joyRideSteps: selectJoyRideSteps
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJoyRideFinished: (steps) => dispatch(setJoyRideFinished(steps))
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyRideFinished }) {
|
||||
export function Manage({ conflict, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [chatVisible] = useState(false);
|
||||
|
||||
@@ -583,21 +576,6 @@ export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoy
|
||||
<UpdateAlert />
|
||||
<HeaderContainer />
|
||||
<Content className="content-container">
|
||||
<Joyride
|
||||
debug
|
||||
run={enableJoyRide}
|
||||
steps={joyRideSteps}
|
||||
continuous
|
||||
hideCloseButton
|
||||
scrollToFirstStep
|
||||
showProgress
|
||||
showSkipButton
|
||||
callback={(props) => {
|
||||
if (props.action === "reset") {
|
||||
setJoyRideFinished();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<PartnerPingComponent />
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundary />} showDialog>
|
||||
{PageContent}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React from "react";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
import ProductionListTable from "../../components/production-list-table/production-list-table.container";
|
||||
|
||||
export default function ProductionListComponent() {
|
||||
return <ProductionListTable />;
|
||||
return (
|
||||
<>
|
||||
<NoteUpsertModal />
|
||||
<ProductionListTable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ const TimeTicketModalTask = lazy(
|
||||
const TechAssignedProdJobs = lazy(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
|
||||
const TechDispatchedParts = lazy(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
|
||||
|
||||
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -67,6 +69,7 @@ export function TechPage({ technician }) {
|
||||
<UpdateAlert />
|
||||
<TechHeader />
|
||||
<TechLookupJobsDrawer />
|
||||
<TaskUpsertModalContainer />
|
||||
<Content className="tech-content-container">
|
||||
<ErrorBoundary>
|
||||
<Suspense
|
||||
|
||||
@@ -67,11 +67,3 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
|
||||
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
|
||||
payload: isUpdateAvailable
|
||||
});
|
||||
export const setJoyRideSteps = (steps) => ({
|
||||
type: ApplicationActionTypes.SET_JOYRIDE_STEPS,
|
||||
payload: steps
|
||||
});
|
||||
export const setJoyRideFinished = () => ({
|
||||
type: ApplicationActionTypes.SET_JOYRIDE_FINISHED
|
||||
//payload: isUpdateAvailable,
|
||||
});
|
||||
|
||||
@@ -14,9 +14,7 @@ const INITIAL_STATE = {
|
||||
error: null
|
||||
},
|
||||
jobReadOnly: false,
|
||||
partnerVersion: null,
|
||||
enableJoyRide: false,
|
||||
joyRideSteps: []
|
||||
partnerVersion: null
|
||||
};
|
||||
|
||||
const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
@@ -89,12 +87,6 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
|
||||
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
|
||||
return { ...state, problemJobs: action.payload };
|
||||
}
|
||||
case ApplicationActionTypes.SET_JOYRIDE_STEPS: {
|
||||
return { ...state, enableJoyRide: true, joyRideSteps: action.payload };
|
||||
}
|
||||
case ApplicationActionTypes.SET_JOYRIDE_FINISHED: {
|
||||
return { ...state, enableJoyRide: false, joyRideSteps: [] };
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -22,5 +22,3 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
|
||||
export const selectOnline = createSelector([selectApplication], (application) => application.online);
|
||||
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
|
||||
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
|
||||
export const selectEnableJoyRide = createSelector([selectApplication], (application) => application.enableJoyRide);
|
||||
export const selectJoyRideSteps = createSelector([selectApplication], (application) => application.joyRideSteps);
|
||||
|
||||
@@ -12,8 +12,6 @@ const ApplicationActionTypes = {
|
||||
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
|
||||
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
|
||||
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
|
||||
SET_JOYRIDE_STEPS: "SET_JOYRIDE_STEPS",
|
||||
SET_JOYRIDE_FINISHED: "SET_JOYRIDE_FINISHED"
|
||||
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
|
||||
};
|
||||
export default ApplicationActionTypes;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
export const BETA_KEY = "betaSwitchImex";
|
||||
|
||||
export const checkBeta = () => {
|
||||
const cookie = document.cookie.split("; ").find((row) => row.startsWith(BETA_KEY));
|
||||
return cookie ? cookie.split("=")[1] === "true" : false;
|
||||
};
|
||||
|
||||
export const setBeta = (value) => {
|
||||
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
||||
document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`;
|
||||
};
|
||||
|
||||
export const handleBeta = () => {
|
||||
// If the current host name does not start with beta or test, then we don't need to do anything.
|
||||
if (window.location.hostname.startsWith("localhost")) {
|
||||
console.log("Not on beta or test, so no need to handle beta.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isBeta = checkBeta();
|
||||
|
||||
const currentHostName = window.location.hostname;
|
||||
|
||||
// Beta is enabled, but the current host name does start with beta.
|
||||
if (isBeta && !currentHostName.startsWith("beta")) {
|
||||
const href = `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
|
||||
// Beta is not enabled, but the current host name does start with beta.
|
||||
else if (!isBeta && currentHostName.startsWith("beta")) {
|
||||
const href = `${window.location.protocol}//${currentHostName.replace("beta.", "")}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
};
|
||||
export default handleBeta;
|
||||
@@ -11,26 +11,37 @@ export const setBeta = (value) => {
|
||||
};
|
||||
|
||||
export const handleBeta = () => {
|
||||
// If the current host name does not start with beta or test, then we don't need to do anything.
|
||||
if (window.location.hostname.startsWith("localhost")) {
|
||||
console.log("Not on beta or test, so no need to handle beta.");
|
||||
return;
|
||||
}
|
||||
|
||||
const isBeta = checkBeta();
|
||||
|
||||
const currentHostName = window.location.hostname;
|
||||
|
||||
// Beta is enabled, but the current host name does start with beta.
|
||||
if (isBeta && !currentHostName.startsWith("beta")) {
|
||||
const href = `${window.location.protocol}//beta.${currentHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
// Determine if the host name starts with "beta" or "www.beta"
|
||||
const isBetaHost = currentHostName.startsWith("beta.");
|
||||
const isBetaHostWithWWW = currentHostName.startsWith("www.beta.");
|
||||
|
||||
// Beta is not enabled, but the current host name does start with beta.
|
||||
else if (!isBeta && currentHostName.startsWith("beta")) {
|
||||
const href = `${window.location.protocol}//${currentHostName.replace("beta.", "")}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
if (isBeta) {
|
||||
// If beta is on and we are not on a beta domain, redirect to the beta version
|
||||
if (!isBetaHost && !isBetaHostWithWWW) {
|
||||
const newHostName = currentHostName.startsWith("www.")
|
||||
? `www.beta.${currentHostName.replace(/^www\./, "")}`
|
||||
: `beta.${currentHostName}`;
|
||||
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
// Otherwise, if beta is on and we're already on a beta domain, stay there
|
||||
} else {
|
||||
// If beta is off and we are on a beta domain, redirect to the non-beta version
|
||||
if (isBetaHost || isBetaHostWithWWW) {
|
||||
const newHostName = currentHostName.replace(/^www\.beta\./, "www.").replace(/^beta\./, "");
|
||||
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
window.location.replace(href);
|
||||
}
|
||||
// Otherwise, if beta is off and we're not on a beta domain, stay there
|
||||
}
|
||||
};
|
||||
|
||||
export default handleBeta;
|
||||
|
||||
@@ -6,8 +6,6 @@ import * as url from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import { ViteEjsPlugin } from "vite-plugin-ejs";
|
||||
import eslint from "vite-plugin-eslint";
|
||||
|
||||
//import CompressionPlugin from 'vite-plugin-compression';
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
|
||||
|
||||
@@ -16,7 +14,8 @@ process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
|
||||
});
|
||||
|
||||
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
|
||||
function reactVirtualized() {
|
||||
|
||||
function reactVirtualizedFix() {
|
||||
return {
|
||||
name: "flat:react-virtualized",
|
||||
configResolved: async () => {
|
||||
@@ -37,10 +36,7 @@ function reactVirtualized() {
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
plugins: [
|
||||
ViteEjsPlugin((viteConfig) => {
|
||||
// viteConfig is the current Vite resolved config
|
||||
return { env: viteConfig.env };
|
||||
}),
|
||||
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
|
||||
VitePWA({
|
||||
injectRegister: "auto",
|
||||
registerType: "prompt",
|
||||
@@ -60,7 +56,6 @@ export default defineConfig({
|
||||
description: "The ultimate bodyshop management system.",
|
||||
icons: [
|
||||
{
|
||||
//TODO:AIO Ensure that these are correct for Rome and IO.
|
||||
src: InstanceRenderManager({
|
||||
instance: process.env.VITE_APP_INSTANCE,
|
||||
imex: "favicon.png",
|
||||
@@ -101,7 +96,7 @@ export default defineConfig({
|
||||
gcm_sender_id: "103953800507"
|
||||
}
|
||||
}),
|
||||
reactVirtualized(),
|
||||
reactVirtualizedFix(),
|
||||
react(),
|
||||
eslint()
|
||||
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- UPDATE "public"."masterdata"
|
||||
-- SET value = jsonb_set(
|
||||
-- value::jsonb,
|
||||
-- '{OP20}',
|
||||
-- '{"desc": "REMOVE AND REINSTALL", "opcode": "OP20", "partcode": "PAE"}'::jsonb,
|
||||
-- true
|
||||
-- );
|
||||
@@ -0,0 +1,7 @@
|
||||
UPDATE "public"."masterdata"
|
||||
SET value = jsonb_set(
|
||||
value::jsonb,
|
||||
'{OP20}',
|
||||
'{"desc": "REMOVE AND REINSTALL", "opcode": "OP20", "partcode": "PAE"}'::jsonb,
|
||||
true
|
||||
);
|
||||
641
package-lock.json
generated
641
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -19,18 +19,18 @@
|
||||
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-secrets-manager": "^3.616.0",
|
||||
"@aws-sdk/client-ses": "^3.616.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.616.0",
|
||||
"@opensearch-project/opensearch": "^2.10.0",
|
||||
"aws4": "^1.13.0",
|
||||
"axios": "^1.7.2",
|
||||
"@aws-sdk/client-secrets-manager": "^3.632.0",
|
||||
"@aws-sdk/client-ses": "^3.632.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.632.0",
|
||||
"@opensearch-project/opensearch": "^2.11.0",
|
||||
"aws4": "^1.13.1",
|
||||
"axios": "^1.7.4",
|
||||
"better-queue": "^3.8.12",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"canvas": "^2.11.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"cloudinary": "^2.3.0",
|
||||
"cloudinary": "^2.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "2.8.5",
|
||||
@@ -38,24 +38,24 @@
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"firebase-admin": "^12.2.0",
|
||||
"firebase-admin": "^12.3.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"graylog2": "^0.2.1",
|
||||
"inline-css": "^4.0.2",
|
||||
"intuit-oauth": "^4.1.2",
|
||||
"json-2-csv": "^5.5.4",
|
||||
"json-2-csv": "^5.5.5",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-mailjet": "^6.0.5",
|
||||
"node-persist": "^4.0.2",
|
||||
"node-persist": "^4.0.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"phone": "^3.1.49",
|
||||
"phone": "^3.1.50",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"rimraf": "^6.0.1",
|
||||
"soap": "^1.1.0",
|
||||
"soap": "^1.1.1",
|
||||
"socket.io": "^4.7.5",
|
||||
"ssh2-sftp-client": "^10.0.3",
|
||||
"twilio": "^4.23.0",
|
||||
|
||||
@@ -30,8 +30,14 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
|
||||
if (jobline.db_ref === "936007") {
|
||||
hasMashLine = true;
|
||||
}
|
||||
|
||||
//Check if the line is a Towing Line and flag as such.
|
||||
let isTowingLine = false;
|
||||
if (jobline.db_ref === "936001" && jobline.line_desc.includes("Towing")) {
|
||||
isTowingLine = true;
|
||||
}
|
||||
//Parts Lines Mappings.
|
||||
if (jobline.profitcenter_part) {
|
||||
if (!isTowingLine && jobline.profitcenter_part) {
|
||||
//TODO:AIO This appears to be a net 0 change exept for default quantity as 0 instead of 1 for imex. Need to verify.
|
||||
const discountAmount =
|
||||
((jobline.prt_dsmk_m && jobline.prt_dsmk_m !== 0) || (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0)) &&
|
||||
@@ -664,7 +670,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
|
||||
});
|
||||
}
|
||||
} else {
|
||||
//Handle insurance profile adjustments
|
||||
//Handle insurance profile adjustments for Parts
|
||||
Object.keys(job_totals.parts.adjustments).forEach((key) => {
|
||||
if (qbo) {
|
||||
//Going to always assume that we need to apply GST and PST for labor.
|
||||
@@ -718,6 +724,67 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
|
||||
}
|
||||
});
|
||||
|
||||
//Handle insurance profile adjustments for Labor and Materials
|
||||
Object.keys(job_totals.rates).forEach((key) => {
|
||||
if (
|
||||
job_totals.rates[key] &&
|
||||
job_totals.rates[key].adjustment &&
|
||||
Dinero(job_totals.rates[key].adjustment).isZero() === false
|
||||
) {
|
||||
if (qbo) {
|
||||
//Going to always assume that we need to apply GST and PST for labor.
|
||||
const taxAccountCode = findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: process.env.COUNTRY === "USA" ? false : true,
|
||||
state: jobs_by_pk.state_tax_rate === 0 ? false : true
|
||||
},
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes
|
||||
);
|
||||
const account = responsibilityCenters.profits.find(
|
||||
(c) => c.name === responsibilityCenters.defaults.profits[key.toUpperCase()]
|
||||
);
|
||||
const QboTaxId =
|
||||
process.env.COUNTRY === "USA"
|
||||
? CheckQBOUSATaxID({
|
||||
// jobline: jobline,
|
||||
job: jobs_by_pk,
|
||||
type: "storage"
|
||||
})
|
||||
: taxCodes[taxAccountCode];
|
||||
InvoiceLineAdd.push({
|
||||
DetailType: "SalesItemLineDetail",
|
||||
Amount: Dinero(job_totals.rates[key].adjustment).toFormat(DineroQbFormat),
|
||||
Description: `${account.accountdesc} - Adjustment`,
|
||||
SalesItemLineDetail: {
|
||||
...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}),
|
||||
ItemRef: {
|
||||
value: items[account.accountitem]
|
||||
},
|
||||
TaxCodeRef: {
|
||||
value: QboTaxId
|
||||
},
|
||||
Qty: 1
|
||||
}
|
||||
});
|
||||
} else {
|
||||
InvoiceLineAdd.push({
|
||||
ItemRef: {
|
||||
FullName: responsibilityCenters.profits.find(
|
||||
(c) => c.name === responsibilityCenters.defaults.profits[key.toUpperCase()]
|
||||
).accountitem
|
||||
},
|
||||
Desc: "Storage",
|
||||
Quantity: 1,
|
||||
Amount: Dinero(job_totals.rates[key].adjustment).toFormat(DineroQbFormat),
|
||||
SalesTaxCodeRef: {
|
||||
FullName: bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const QboTaxId =
|
||||
process.env.COUNTRY === "USA"
|
||||
? CheckQBOUSATaxID({
|
||||
|
||||
@@ -273,7 +273,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
||||
return result && result.json && result.json.Bill;
|
||||
} catch (error) {
|
||||
logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, {
|
||||
error: (error && error.authResponse && error.authResponse.body) || (error && error.message),
|
||||
error: error, //(error && error.authResponse && error.authResponse.body) || (error && error.message),
|
||||
method: "InsertBill"
|
||||
});
|
||||
throw error;
|
||||
|
||||
@@ -360,12 +360,10 @@ function calculateAllocations(connectionData, job) {
|
||||
}
|
||||
}
|
||||
if (InstanceManager({ rome: true })) {
|
||||
//profile level adjustments
|
||||
//profile level adjustments for parts
|
||||
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
|
||||
const accountName = selectedDmsAllocationConfig.profits[key];
|
||||
|
||||
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
|
||||
|
||||
if (otherAccount) {
|
||||
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
|
||||
|
||||
@@ -380,6 +378,24 @@ function calculateAllocations(connectionData, job) {
|
||||
);
|
||||
}
|
||||
});
|
||||
//profile level adjustments for labor and materials
|
||||
Object.keys(job.job_totals.rates).forEach((key) => {
|
||||
if (job.job_totals.rates[key] && job.job_totals.rates[key].adjustment && Dinero(job.job_totals.rates[key].adjustment).isZero() === false) {
|
||||
const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()];
|
||||
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
|
||||
if (otherAccount) {
|
||||
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
|
||||
|
||||
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates[key].adjustments));
|
||||
} else {
|
||||
CdkBase.createLogEvent(
|
||||
connectionData,
|
||||
"ERROR",
|
||||
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {
|
||||
|
||||
@@ -33,7 +33,7 @@ const ftpSetup = {
|
||||
exports.default = async (req, res) => {
|
||||
//Query for the List of Bodyshop Clients.
|
||||
logger.log("kaizen-start", "DEBUG", "api", null, null);
|
||||
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE"];
|
||||
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"];
|
||||
|
||||
const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, {
|
||||
imexshopid: kaizenShopsIDs
|
||||
|
||||
@@ -1527,6 +1527,8 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
|
||||
ca_bc_pvrt
|
||||
ca_customer_gst
|
||||
dms_allocation
|
||||
cieca_pfl
|
||||
materials
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
db_ref
|
||||
@@ -1641,6 +1643,8 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
|
||||
ca_bc_pvrt
|
||||
ca_customer_gst
|
||||
dms_allocation
|
||||
cieca_pfl
|
||||
materials
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
id
|
||||
db_ref
|
||||
@@ -1866,6 +1870,8 @@ exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
|
||||
scheduled_in
|
||||
actual_in
|
||||
ca_bc_pvrt
|
||||
cieca_pfl
|
||||
materials
|
||||
timetickets {
|
||||
id
|
||||
actualhrs
|
||||
|
||||
@@ -286,9 +286,45 @@ function GenerateCostingData(job) {
|
||||
|
||||
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
|
||||
|
||||
const laborAmount = Dinero({
|
||||
let laborAmount = Dinero();
|
||||
laborAmount = Dinero({
|
||||
amount: Math.round((job[rateName] || 0) * 100)
|
||||
}).multiply(val.mod_lb_hrs || 0);
|
||||
|
||||
if (
|
||||
job.cieca_pfl &&
|
||||
job.cieca_pfl[val.mod_lbr_ty.toUpperCase()] &&
|
||||
job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp !== 0
|
||||
) {
|
||||
let adjp = 0;
|
||||
if (
|
||||
val.mod_lbr_ty === "la1" ||
|
||||
val.mod_lbr_ty === "la2" ||
|
||||
val.mod_lbr_ty === "la3" ||
|
||||
val.mod_lbr_ty === "la4"
|
||||
) {
|
||||
adjp =
|
||||
Math.abs(job.cieca_pfl["LAU"].lbr_adjp) > 1
|
||||
? job.cieca_pfl["LAU"].lbr_adjp
|
||||
: job.cieca_pfl["LAU"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
|
||||
} else {
|
||||
if (job.cieca_pfl[val.mod_lbr_ty.toUpperCase()]) {
|
||||
adjp =
|
||||
Math.abs(job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp) > 1
|
||||
? job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp
|
||||
: job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp * 100; //Adjust lbr_adjp to whole number
|
||||
} else {
|
||||
adjp =
|
||||
Math.abs(job.cieca_pfl["LAB"].lbr_adjp) > 1
|
||||
? job.cieca_pfl["LAB"].lbr_adjp
|
||||
: job.cieca_pfl["LAB"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
|
||||
}
|
||||
}
|
||||
laborAmount = laborAmount.add(
|
||||
laborAmount.percentage(adjp < 0 ? adjp * -1 : adjp).multiply(adjp < 0 ? -1 : 1)
|
||||
);
|
||||
}
|
||||
|
||||
if (!acc.labor[laborProfitCenter]) acc.labor[laborProfitCenter] = Dinero();
|
||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(laborAmount);
|
||||
|
||||
@@ -317,7 +353,7 @@ function GenerateCostingData(job) {
|
||||
|
||||
if (!partsProfitCenter)
|
||||
console.log("Unknown cost/profit center mapping for parts.", val.line_desc, val.part_type);
|
||||
const partsAmount = Dinero({
|
||||
let partsAmount = Dinero({
|
||||
amount: val.act_price_before_ppc
|
||||
? Math.round(val.act_price_before_ppc * 100)
|
||||
: Math.round(val.act_price * 100)
|
||||
@@ -338,6 +374,33 @@ function GenerateCostingData(job) {
|
||||
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
|
||||
: Dinero()
|
||||
);
|
||||
|
||||
// Profile Discount for Parts
|
||||
if (job.parts_tax_rates && job.parts_tax_rates[val.part_type.toUpperCase()]) {
|
||||
if (
|
||||
job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp !== undefined &&
|
||||
job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp >= 0
|
||||
) {
|
||||
const discountRate =
|
||||
Math.abs(job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp) > 1
|
||||
? parts_tajob.parts_tax_rates_rates[val.part_type.toUpperCase()].prt_discp
|
||||
: job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp * 100;
|
||||
const disc = partsAmount.percentage(discountRate).multiply(-1);
|
||||
partsAmount = partsAmount.add(disc);
|
||||
}
|
||||
if (
|
||||
job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp !== undefined &&
|
||||
job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp >= 0
|
||||
) {
|
||||
const markupRate =
|
||||
Math.abs(job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp) > 1
|
||||
? job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp
|
||||
: job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp * 100;
|
||||
const markup = partsAmount.percentage(markupRate);
|
||||
partsAmount = partsAmount.add(markup);
|
||||
}
|
||||
}
|
||||
|
||||
if (!acc.parts[partsProfitCenter]) acc.parts[partsProfitCenter] = Dinero();
|
||||
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(partsAmount);
|
||||
}
|
||||
@@ -413,6 +476,7 @@ function GenerateCostingData(job) {
|
||||
if (!hasMapaLine) {
|
||||
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]])
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero();
|
||||
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = jobLineTotalsByProfitCenter.additional[
|
||||
defaultProfits["MAPA"]
|
||||
].add(
|
||||
@@ -420,10 +484,26 @@ function GenerateCostingData(job) {
|
||||
amount: Math.round((job.rate_mapa || 0) * 100)
|
||||
}).multiply(materialsHours.mapaHrs || 0)
|
||||
);
|
||||
let adjp = 0;
|
||||
if (job.materials["MAPA"] && job.materials["MAPA"].mat_adjp) {
|
||||
adjp =
|
||||
Math.abs(job.materials["MAPA"].mat_adjp) > 1
|
||||
? job.materials["MAPA"].mat_adjp
|
||||
: job.materials["MAPA"].mat_adjp * 100; //Adjust mat_adjp to whole number
|
||||
}
|
||||
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = jobLineTotalsByProfitCenter.additional[
|
||||
defaultProfits["MAPA"]
|
||||
].add(
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]]
|
||||
.percentage(adjp < 0 ? adjp * -1 : adjp)
|
||||
.multiply(adjp < 0 ? -1 : 1)
|
||||
);
|
||||
}
|
||||
if (!hasMashLine) {
|
||||
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]])
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero();
|
||||
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = jobLineTotalsByProfitCenter.additional[
|
||||
defaultProfits["MASH"]
|
||||
].add(
|
||||
@@ -431,6 +511,21 @@ function GenerateCostingData(job) {
|
||||
amount: Math.round((job.rate_mash || 0) * 100)
|
||||
}).multiply(materialsHours.mashHrs || 0)
|
||||
);
|
||||
let adjp = 0;
|
||||
if (job.materials["MASH"] && job.materials["MASH"].mat_adjp) {
|
||||
adjp =
|
||||
Math.abs(job.materials["MASH"].mat_adjp) > 1
|
||||
? job.materials["MASH"].mat_adjp
|
||||
: job.materials["MASH"].mat_adjp * 100; //Adjust mat_adjp to whole number
|
||||
}
|
||||
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = jobLineTotalsByProfitCenter.additional[
|
||||
defaultProfits["MASH"]
|
||||
].add(
|
||||
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]]
|
||||
.percentage(adjp < 0 ? adjp * -1 : adjp)
|
||||
.multiply(adjp < 0 ? -1 : 1)
|
||||
);
|
||||
}
|
||||
|
||||
//Is it a DMS Setup?
|
||||
|
||||
@@ -331,6 +331,8 @@ async function CalculateRatesTotals({ job, client }) {
|
||||
//Skip calculating mapa and mash if we got the amounts.
|
||||
if (!((property === "mapa" && hasMapaLine) || (property === "mash" && hasMashLine))) {
|
||||
if (!ret[property].total) {
|
||||
ret[property].base = Dinero();
|
||||
ret[property].adjustment = Dinero();
|
||||
ret[property].total = Dinero();
|
||||
}
|
||||
let threshold;
|
||||
@@ -349,13 +351,50 @@ async function CalculateRatesTotals({ job, client }) {
|
||||
}
|
||||
}
|
||||
|
||||
const total = Dinero({
|
||||
const base = Dinero({
|
||||
amount: Math.round((ret[property].rate || 0) * 100)
|
||||
}).multiply(ret[property].hours);
|
||||
|
||||
let adjp = 0;
|
||||
if (property === "mapa" || property === "mash") {
|
||||
if (job.materials[property.toUpperCase()] && job.materials[property.toUpperCase()].mat_adjp) {
|
||||
adjp =
|
||||
Math.abs(job.materials[property.toUpperCase()].mat_adjp) > 1
|
||||
? job.materials[property.toUpperCase()].mat_adjp
|
||||
: job.materials[property.toUpperCase()].mat_adjp * 100; //Adjust mat_adjp to whole number
|
||||
}
|
||||
} else {
|
||||
if (property === "la1" || property === "la2" || property === "la3" || property === "la4") {
|
||||
if (job.cieca_pfl["LAU"] && job.cieca_pfl["LAU"].lbr_adjp) {
|
||||
adjp =
|
||||
Math.abs(job.cieca_pfl["LAU"].lbr_adjp) > 1
|
||||
? job.cieca_pfl["LAU"].lbr_adjp
|
||||
: job.cieca_pfl["LAU"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
|
||||
}
|
||||
} else {
|
||||
if (job.cieca_pfl[property.toUpperCase()] && job.cieca_pfl[property.toUpperCase()].lbr_adjp) {
|
||||
adjp =
|
||||
Math.abs(job.cieca_pfl[property.toUpperCase()].lbr_adjp) > 1
|
||||
? job.cieca_pfl[property.toUpperCase()].lbr_adjp
|
||||
: job.cieca_pfl[property.toUpperCase()].lbr_adjp * 100; //Adjust lbr_adjp to whole number
|
||||
} else {
|
||||
if (job.cieca_pfl["LAB"].lbr_adjp) {
|
||||
adjp =
|
||||
Math.abs(job.cieca_pfl["LAB"].lbr_adjp) > 1
|
||||
? job.cieca_pfl["LAB"].lbr_adjp
|
||||
: job.cieca_pfl["LAB"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const adjustment = base.percentage(adjp < 0 ? adjp * -1 : adjp).multiply(adjp < 0 ? -1 : 1);
|
||||
const total = base.add(adjustment);
|
||||
|
||||
if (threshold && total.greaterThanOrEqual(threshold)) {
|
||||
ret[property].total = ret[property].total.add(threshold);
|
||||
} else {
|
||||
ret[property].base = ret[property].base.add(base);
|
||||
ret[property].adjustment = ret[property].adjustment.add(adjustment);
|
||||
ret[property].total = ret[property].total.add(total);
|
||||
}
|
||||
}
|
||||
@@ -703,18 +742,19 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
//Potential issue here with Sublet Calculation. Sublets are calculated under labor in Mitchell, but it's done in IO
|
||||
//Under the parts rates.
|
||||
|
||||
let statePartsTax = Dinero();
|
||||
let additionalItemsTax = Dinero();
|
||||
let stateTax = Dinero();
|
||||
// let additionalItemsTax = Dinero(); //This is not used.
|
||||
let us_sales_tax_breakdown;
|
||||
|
||||
// This is not referenced in the code base.
|
||||
//Audatex sends additional glass part types. IO-774
|
||||
const BackupGlassTax =
|
||||
job.parts_tax_rates &&
|
||||
(job.parts_tax_rates.PAGD ||
|
||||
job.parts_tax_rates.PAGF ||
|
||||
job.parts_tax_rates.PAGP ||
|
||||
job.parts_tax_rates.PAGQ ||
|
||||
job.parts_tax_rates.PAGR);
|
||||
// const BackupGlassTax =
|
||||
// job.parts_tax_rates &&
|
||||
// (job.parts_tax_rates.PAGD ||
|
||||
// job.parts_tax_rates.PAGF ||
|
||||
// job.parts_tax_rates.PAGP ||
|
||||
// job.parts_tax_rates.PAGQ ||
|
||||
// job.parts_tax_rates.PAGR);
|
||||
|
||||
const taxableAmounts = {
|
||||
PAA: Dinero(),
|
||||
@@ -878,11 +918,27 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
} else if (key.startsWith("LA")) {
|
||||
//Labor.
|
||||
for (let tyCounter = 1; tyCounter <= 5; tyCounter++) {
|
||||
if (IsTrueOrYes(pfl[key][`lbr_tx_in${tyCounter}`])) {
|
||||
//This amount is taxable for this type.
|
||||
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
|
||||
taxableAmounts[key]
|
||||
);
|
||||
if (key === "LA1" || key === "LA2" || key === "LA3" || key === "LA4") {
|
||||
if (IsTrueOrYes(pfl["LAU"][`lbr_tx_in${tyCounter}`])) {
|
||||
//This amount is taxable for this type.
|
||||
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
|
||||
taxableAmounts[key]
|
||||
);
|
||||
}
|
||||
} else if (key === "LAA" && !pfl[key]) {
|
||||
if (IsTrueOrYes(pfl["LAB"][`lbr_tx_in${tyCounter}`])) {
|
||||
//This amount is taxable for this type.
|
||||
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
|
||||
taxableAmounts[key]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (IsTrueOrYes(pfl[key][`lbr_tx_in${tyCounter}`])) {
|
||||
//This amount is taxable for this type.
|
||||
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
|
||||
taxableAmounts[key]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (key === "TOW") {
|
||||
@@ -919,7 +975,6 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
|
||||
Object.keys(taxableAmountsByTier).forEach((taxTierKey) => {
|
||||
taxable_adjustment = taxableAmountsByTier[taxTierKey].multiply(percent_of_adjustment);
|
||||
console.log("🚀 ~ taxableAmountsByTier ~ taxable_adjustment:", taxable_adjustment);
|
||||
if (job.adjustment_bottom_line > 0) {
|
||||
taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].add(taxable_adjustment);
|
||||
} else {
|
||||
@@ -937,8 +992,8 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
let tyCounter = taxTierKey[2]; //Get the number from the key.
|
||||
//i represents the tax number. If we got here, this type of tax is applicable. Now we need to add based on the thresholds.
|
||||
for (let threshCounter = 1; threshCounter <= 5; threshCounter++) {
|
||||
const thresholdAmount = parseFloat(job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]);
|
||||
const thresholdTaxRate = parseFloat(job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]);
|
||||
const thresholdAmount = parseFloat(job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]) || 0;
|
||||
const thresholdTaxRate = parseFloat(job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]) || 0;
|
||||
|
||||
let taxableAmountInThisThreshold;
|
||||
if (
|
||||
@@ -946,7 +1001,7 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
InstanceMgr({
|
||||
imex: false,
|
||||
rome: thresholdAmount === 0 && parseInt(tyCounter) === 1,
|
||||
promanager: thresholdAmount === 0 && parseInt(tyCounter) === 1
|
||||
promanager: "USE_ROME"
|
||||
})
|
||||
) {
|
||||
//
|
||||
@@ -980,10 +1035,10 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("*** Total Tax by Tier Amounts***");
|
||||
// console.table(JSON.parse(JSON.stringify(totalTaxByTier)));
|
||||
console.log("*** Total Tax by Tier Amounts***");
|
||||
console.table(JSON.parse(JSON.stringify(totalTaxByTier)));
|
||||
|
||||
statePartsTax = statePartsTax
|
||||
stateTax = stateTax
|
||||
.add(totalTaxByTier.ty1Tax)
|
||||
.add(totalTaxByTier.ty2Tax)
|
||||
.add(totalTaxByTier.ty3Tax)
|
||||
@@ -991,17 +1046,18 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
.add(totalTaxByTier.ty5Tax)
|
||||
.add(totalTaxByTier.ty6Tax);
|
||||
us_sales_tax_breakdown = totalTaxByTier;
|
||||
//console.log("Tiered Taxes Total for Parts/Labor", statePartsTax.toFormat());
|
||||
//console.log("Tiered Taxes Total for Parts/Labor", stateTax.toFormat());
|
||||
|
||||
let laborTaxTotal = Dinero();
|
||||
// This is not in use as such commented out.
|
||||
// let laborTaxTotal = Dinero();
|
||||
|
||||
if (Object.keys(job.cieca_pfl).length > 0) {
|
||||
//Ignore it now, we have calculated it above.
|
||||
//This was previously used for JCS before parts were also calculated at a different rate.
|
||||
} else {
|
||||
//We don't have it, just add in how it was before.
|
||||
laborTaxTotal = otherTotals.rates.subtotal.percentage((job.tax_lbr_rt || 0) * 100); // THis is currently using the lbr tax rate from PFH not PFL.
|
||||
}
|
||||
// if (Object.keys(job.cieca_pfl).length > 0) {
|
||||
// //Ignore it now, we have calculated it above.
|
||||
// //This was previously used for JCS before parts were also calculated at a different rate.
|
||||
// } else {
|
||||
// //We don't have it, just add in how it was before.
|
||||
// laborTaxTotal = otherTotals.rates.subtotal.percentage((job.tax_lbr_rt || 0) * 100); // THis is currently using the lbr tax rate from PFH not PFL.
|
||||
// }
|
||||
|
||||
//console.log("Labor Tax Total", laborTaxTotal.toFormat());
|
||||
|
||||
@@ -1010,9 +1066,9 @@ function CalculateTaxesTotals(job, otherTotals) {
|
||||
federal_tax: subtotal
|
||||
.percentage((job.federal_tax_rate || 0) * 100)
|
||||
.add(otherTotals.additional.pvrt.percentage((job.federal_tax_rate || 0) * 100)),
|
||||
statePartsTax,
|
||||
stateTax,
|
||||
us_sales_tax_breakdown,
|
||||
state_tax: statePartsTax,
|
||||
state_tax: stateTax,
|
||||
local_tax: subtotal.percentage((job.local_tax_rate || 0) * 100)
|
||||
};
|
||||
ret.total_repairs = ret.subtotal
|
||||
|
||||
Reference in New Issue
Block a user