Compare commits

..

1 Commits

Author SHA1 Message Date
Dave Richer
779108b94c - add kanbanchild to jobs table
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-18 12:43:58 -04:00
130 changed files with 19974 additions and 22391 deletions

View File

@@ -1,41 +0,0 @@
# 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

View File

@@ -1,7 +1,7 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715

View File

@@ -1,177 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<link rel="icon" href="/ro-favicon.png"/>
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<link rel="icon" href="/pm/pm-favicon.ico"/>
<% } %>
<head>
<meta charset="utf-8" />
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<link rel="icon" href="/favicon.png" />
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<link rel="icon" href="/ro-favicon.png" />
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<link rel="icon" href="/pm/pm-favicon.ico" />
<% } %>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#1690ff"/>
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<!-- TODO:AIo Update the individual logos for each.-->
<link rel="apple-touch-icon" href="public/logo192.png"/>
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#1690ff" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<!-- TODO:AIo Update the individual logos for each.-->
<link rel="apple-touch-icon" href="public/logo192.png" />
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<meta name="description" content="ImEX Online"/>
<title>ImEX Online</title>
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<meta name="description" content="Rome Online"/>
<title>Rome Online</title>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
<meta name="description" content="ImEX Online" />
<title>ImEX Online</title>
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
<meta name="description" content="Rome Online" />
<title>Rome Online</title>
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001
party="LiveChat528346"></call-us-selector>
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
<!--<call-us
phonesystem-url=https://rometech.east.3cx.us:5001
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
id="wp-live-chat-by-3CX"
minimized="true"
animation-style="noanimation"
party="LiveChat528346"
minimized-style="bubbleright"
allow-call="true"
allow-video="false"
allow-soundnotifications="true"
enable-mute="true"
enable-onmobile="true"
offline-enabled="true"
enable="true"
ignore-queueownership="false"
authentication="both"
show-operator-actual-name="true"
aknowledge-received="true"
gdpr-enabled="false"
message-userinfo-format="name"
message-dateformat="both"
lang="browser"
button-icon-type="default"
greeting-visibility="none"
greeting-offline-visibility="none"
chat-delay="2000"
enable-direct-call="true"
enable-ga="false"
></call-us>-->
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js
id="tcx-callus-js" charset="utf-8"></script>
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
<!--<call-us
phonesystem-url=https://rometech.east.3cx.us:5001
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
id="wp-live-chat-by-3CX"
minimized="true"
animation-style="noanimation"
party="LiveChat528346"
minimized-style="bubbleright"
allow-call="true"
allow-video="false"
allow-soundnotifications="true"
enable-mute="true"
enable-onmobile="true"
offline-enabled="true"
enable="true"
ignore-queueownership="false"
authentication="both"
show-operator-actual-name="true"
aknowledge-received="true"
gdpr-enabled="false"
message-userinfo-format="name"
message-dateformat="both"
lang="browser"
button-icon-type="default"
greeting-visibility="none"
greeting-offline-visibility="none"
chat-delay="2000"
enable-direct-call="true"
enable-ga="false"
></call-us>-->
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js id="tcx-callus-js" charset="utf-8"></script>
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<title>ProManager</title>
<meta name="description" content="ProManager"/>
<% } %>
<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 || []);
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
<title>ProManager</title>
<meta name="description" content="ProManager" />
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);
<% } %>
<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 () {
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>
!(function () {
for (var o = 0; o < e.length; o++) {
var r = e[o];
n[r] = t(r);
}
})(),
(function () {
var e = document.createElement('script');
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
var n = document.head;
n.insertBefore(e, n.firstChild);
})();
}
})();
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="src/index.jsx"></script>
</body>
<script type="module" src="src/index.jsx"></script>
</body>
</html>

1404
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,78 +8,79 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.4",
"@emotion/is-prop-valid": "^1.3.0",
"@ant-design/pro-layout": "^7.19.10",
"@apollo/client": "^3.10.8",
"@emotion/is-prop-valid": "^1.2.2",
"@fingerprintjs/fingerprintjs": "^4.4.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.33.1",
"@reduxjs/toolkit": "^2.2.6",
"@sentry/cli": "^2.32.2",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.12.1",
"@splitsoftware/splitio-react": "^1.12.0",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.1",
"antd": "^5.19.2",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
"axios": "^1.7.4",
"axios": "^1.6.8",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.12",
"dayjs": "^1.11.11",
"dayjs-business-days2": "^1.2.2",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.12.5",
"firebase": "^10.12.3",
"graphql": "^16.9.0",
"i18next": "^23.12.3",
"i18next": "^23.12.1",
"i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.5",
"logrocket": "^8.1.2",
"libphonenumber-js": "^1.11.4",
"logrocket": "^8.1.0",
"markerjs2": "^2.32.1",
"memoize-one": "^6.0.0",
"normalize-url": "^8.0.1",
"object-hash": "^3.0.0",
"prop-types": "^15.8.1",
"query-string": "^9.1.0",
"query-string": "^9.0.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.13.2",
"react-big-calendar": "^1.13.1",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-cookie": "^7.1.4",
"react-dom": "^18.3.1",
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^14.1.3",
"react-icons": "^5.3.0",
"react-icons": "^5.2.1",
"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-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.26.0",
"react-router-dom": "^6.24.1",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.1",
"react-virtuoso": "^4.7.12",
"recharts": "^2.12.7",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
"redux-actions": "^3.0.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.77.8",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.12",
"styled-components": "^6.1.11",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.5",
"userpilot": "^1.3.2",
"vite-plugin-ejs": "^1.7.0",
"web-vitals": "^3.5.2"
},
@@ -130,29 +131,29 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.7.0",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.0",
"@sentry/webpack-plugin": "^2.22.2",
"@dotenvx/dotenvx": "^1.6.4",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/react": "^11.11.4",
"@sentry/webpack-plugin": "^2.21.1",
"@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3",
"browserslist": "^4.23.2",
"browserslist-to-esbuild": "^2.1.1",
"cross-env": "^7.0.3",
"cypress": "^13.13.3",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
"memfs": "^4.11.1",
"memfs": "^4.9.3",
"os-browserify": "^0.3.0",
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.4.0",
"vite": "^5.3.4",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-style-import": "^2.0.0"
}
}

View File

@@ -1,13 +1,15 @@
import { useSplitClient } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd";
import LogRocket from "logrocket";
import React, { lazy, Suspense, useEffect, useState } from "react";
import React, { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Routes } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
// Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
@@ -18,10 +20,10 @@ 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/handleBeta";
import handleBeta from "../utils/betaHandler";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { ProductFruits } from "react-product-fruits";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
@@ -46,6 +48,23 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
const [listenersAdded, setListenersAdded] = useState(false);
const { t } = useTranslation();
const workspaceCode = useMemo(
() =>
InstanceRenderMgr({
imex: null,
rome: "9BkbEseqNqxw8jUH",
promanager: "aoJoEifvezYI0Z0P"
}),
[]
);
const workspaceLogin = useMemo(
() => ({
email: currentUser.email,
username: currentUser.email
}),
[currentUser.email]
);
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
@@ -54,10 +73,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
checkUserSession();
}, [checkUserSession, setOnline]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
// Associate event listeners, memoize to prevent multiple listeners being added
useEffect(() => {
const offlineListener = () => {
setOnline(false);
@@ -129,7 +144,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
return <Eula />;
}
// Any route that is not assigned and matched will default to the Landing Page component
return (
<Suspense
fallback={
@@ -142,14 +156,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
/>
}
>
<ProductFruitsWrapper
currentUser={currentUser}
workspaceCode={InstanceRenderMgr({
imex: null,
rome: "9BkbEseqNqxw8jUH",
promanager: "aoJoEifvezYI0Z0P"
})}
/>
{currentUser && currentUser.email && (
<ProductFruits workspaceCode={workspaceCode} debug language="en" user={workspaceLogin} />
)}
<Routes>
<Route

View File

@@ -161,15 +161,3 @@
.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;
}

View File

@@ -1,32 +0,0 @@
import React from "react";
import { ProductFruits } from "react-product-fruits";
import PropTypes from "prop-types";
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
return (
workspaceCode &&
currentUser?.authorized === true &&
currentUser?.email && (
<ProductFruits
lifeCycle="unmount"
workspaceCode={workspaceCode}
debug
language="en"
user={{
email: currentUser.email,
username: currentUser.email
}}
/>
)
);
});
export default ProductFruitsWrapper;
ProductFruitsWrapper.propTypes = {
currentUser: PropTypes.shape({
authorized: PropTypes.bool,
email: PropTypes.string
}),
workspaceCode: PropTypes.string
};

View File

@@ -13,7 +13,6 @@ 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";
@@ -171,22 +170,13 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
extra={
<Space wrap>
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
<>
<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}
/>
</>
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input.Search

View File

@@ -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,7 +13,6 @@ 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";
@@ -23,6 +22,7 @@ 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,7 +153,6 @@ 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 (
<>
@@ -189,7 +188,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
}
/>
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse} />
<BillFormContainer form={form} billEdit disabled={exported} />
<Divider orientation="left">{t("general.labels.media")}</Divider>
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery

View File

@@ -14,6 +14,7 @@ import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
@@ -21,7 +22,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -41,8 +41,7 @@ export function BillFormComponent({
job,
loadOutstandingReturns,
loadInventory,
preferredMake,
disableInHouse
preferredMake
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -178,7 +177,7 @@ export function BillFormComponent({
]}
>
<VendorSearchSelect
disabled={disabled || disableInHouse}
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
@@ -244,7 +243,7 @@ export function BillFormComponent({
})
]}
>
<Input disabled={disabled || disableInvNumber || disableInHouse} />
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t("bills.fields.date")}
@@ -276,7 +275,7 @@ export function BillFormComponent({
})
]}
>
<DateTimePicker isDateOnly disabled={disabled} />
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}

View File

@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
const {
treatments: { Simple_Inventory }
} = useSplitTreatments({
@@ -47,7 +47,6 @@ 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}

View File

@@ -8,8 +8,8 @@ import { DateFormatter } from "../../utils/DateFormatter";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -196,7 +196,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
{dlExpiresBeforeReturn && (
<Space style={{ color: "tomato" }}>
@@ -274,7 +274,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputPhone />
</Form.Item>
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<ContractsRatesChangeButton form={form} />

View File

@@ -10,12 +10,16 @@ import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function CourtesyCarCreateFormComponent({ form, saveLoading, newCC }) {
export default function CourtesyCarCreateFormComponent({
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -157,16 +161,16 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
<Input />
</Form.Item>
<Form.Item label={t("courtesycars.fields.purchasedate")} name="purchasedate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.servicestartdate")} name="servicestartdate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.serviceenddate")} name="serviceenddate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.leaseenddate")} name="leaseenddate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
@@ -224,7 +228,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
</div>
<div>
<Form.Item label={t("courtesycars.fields.nextservicedate")} name="nextservicedate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}>
{() => {
@@ -256,7 +260,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
</Form.Item>
<div>
<Form.Item label={t("courtesycars.fields.registrationexpires")} name="registrationexpires">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.registrationexpires !== c.registrationexpires}>
{() => {
@@ -289,7 +293,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}>
{() => {

View File

@@ -2,7 +2,7 @@ import { Form, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation();
@@ -19,7 +19,7 @@ export default function CourtesyCarReturnModalComponent() {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmend")}

View File

@@ -36,6 +36,9 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
};
});
console.log("Scheduled Out Today");
console.dir(scheduledOutToday);
const tvFontSize = 18;
const tvFontWeight = "bold";

View File

@@ -24,9 +24,9 @@ import i18n from "../../translations/i18n";
import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -164,7 +164,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
<Input disabled />
</Form.Item>
<Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<Space>

View File

@@ -4,6 +4,7 @@ import Markdown from "react-markdown";
import { createStructuredSelector } from "reselect";
import { selectCurrentEula, selectCurrentUser } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component";
import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries";
import { useMutation } from "@apollo/client";
import { acceptEula } from "../../redux/user/user.actions";
@@ -11,7 +12,6 @@ import { useTranslation } from "react-i18next";
import day from "../../utils/day";
import "./eula.styles.scss";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const Eula = ({ currentEula, currentUser, acceptEula }) => {
const [formReady, setFormReady] = useState(false);
@@ -216,7 +216,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
}
]}
>
<DateTimePicker isDateOnly onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
<FormDatePicker onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
</Form.Item>
</Col>
</Row>

View File

@@ -0,0 +1,123 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker);
const dateFormat = "MM/DD/YYYY";
export function FormDatePicker({
bodyshop,
value,
onChange,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate);
}
};
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "t") {
if (onChange) {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else if (e.key.toLowerCase() === "enter") {
if (ref.current && ref.current.blur) ref.current.blur();
}
};
const handleBlur = (e) => {
const v = e.target.value;
if (!v) return;
const formats = [
"MMDDYY",
"MMDDYYYY",
"MM/DD/YY",
"MM/DD/YYYY",
"M/DD/YY",
"M/DD/YYYY",
"MM/D/YY",
"MM/D/YYYY",
"M/D/YY",
"M/D/YYYY",
"D/MM/YY",
"D/MM/YYYY",
"DD/M/YY",
"DD/M/YYYY",
"D/M/YY",
"D/M/YYYY"
];
let _a;
// Iterate through formats to find the correct one
for (let format of formats) {
_a = dayjs(v, format);
if (v === _a.format(format)) {
break;
}
}
if (_a.isValid() && value && value.isValid && value.isValid()) {
_a.set({
hours: value.hours(),
minutes: value.minutes(),
seconds: value.seconds(),
milliseconds: value.milliseconds()
});
}
if (_a.isValid() && onChange) {
if (onlyFuture) {
if (dayjs().subtract(1, "day").isBefore(_a)) {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
} else {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
}
}
};
return (
<div onKeyDown={handleKeyDown}>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur || handleBlur}
showToday={false}
disabledTime
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day.js";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDateTimePickerEnhanced);
const dateFormat = "MM/DD/YYYY h:mm a";
export function FormDateTimePickerEnhanced({
bodyshop,
value,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
return (
<div>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
format={dateFormat}
onBlur={onBlur}
showToday={false}
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -1,105 +1,46 @@
import { DatePicker } from "antd";
import PropTypes from "prop-types";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import React, { forwardRef } from "react";
//import DatePicker from "react-datepicker";
//import "react-datepicker/src/stylesheets/datepicker.scss";
import { Space, TimePicker } from "antd";
import dayjs from "../../utils/day";
import { fuzzyMatchDate } from "./formats.js";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
//To be used as a form element only.
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => {
const [isManualInput, setIsManualInput] = useState(false);
const { t } = useTranslation();
const handleChange = useCallback(
(newDate) => {
if (onChange) {
onChange(newDate || null);
}
setIsManualInput(false);
},
[onChange]
);
const handleBlur = useCallback(
(e) => {
// Bail if this is not a manual input
if (!isManualInput) {
return;
}
// Reset manual input flag
setIsManualInput(false);
const v = e?.target?.value;
if (!v) return;
let parsedDate = isDateOnly ? fuzzyMatchDate(v)?.startOf("day") : fuzzyMatchDate(v);
if (parsedDate && onChange) {
onChange(parsedDate);
}
},
[isManualInput, isDateOnly, onChange]
);
const handleKeyDown = useCallback(
(e) => {
setIsManualInput(true);
if (e.key.toLowerCase() === "t" && onChange) {
e.preventDefault();
setIsManualInput(false);
onChange(dayjs());
} else if (e.key.toLowerCase() === "enter") {
handleBlur(e);
}
},
[onChange, handleBlur]
);
const handleDisabledDate = useCallback(
(current) => {
if (onlyToday) {
return !dayjs().isSame(current, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(current);
}
return false;
},
[onlyToday, onlyFuture]
);
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps }, ref) => {
// const handleChange = (newDate) => {
// if (value !== newDate && onChange) {
// onChange(newDate);
// }
// };
return (
<div onKeyDown={handleKeyDown} id={id} style={{ width: "100%" }}>
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
<Space direction="vertical" style={{ width: "100%" }} id={id}>
<FormDatePicker
{...restProps}
{...(onlyFuture && {
disabledDate: (d) => dayjs().subtract(1, "day").isAfter(d)
})}
value={value}
onBlur={onBlur}
onChange={onChange}
onlyFuture={onlyFuture}
isDateOnly={false}
/>
<TimePicker
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...(onlyFuture && {
disabledDate: (d) => dayjs().isAfter(d)
})}
onChange={onChange}
disableSeconds={true}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"
{...restProps}
/>
</div>
</Space>
);
};
DateTimePicker.propTypes = {
value: PropTypes.any,
onChange: PropTypes.func,
onBlur: PropTypes.func,
id: PropTypes.string,
onlyFuture: PropTypes.bool,
onlyToday: PropTypes.bool,
isDateOnly: PropTypes.bool
};
export default React.memo(DateTimePicker);
export default forwardRef(DateTimePicker);

View File

@@ -1,63 +0,0 @@
import dayjs from "../../utils/day";
const dateFormats = [
"MMDDYYYY",
"MMDDYY",
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY"
];
const timeFormats = ["h:mm A", "h:mmA", "h A", "hA", "hh:mm A", "hh:mm:ss A"];
const dateTimeFormats = [
...["M/D/YYYY", "MM/D/YYYY", "M/DD/YYYY", "MM/DD/YYYY", "M/D/YY", "MM/D/YY", "M/DD/YY", "MM/DD/YY"].flatMap(
(dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)
),
...["MMDDYYYY", "MMDDYY"].flatMap((dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)),
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY",
"MMDDYYYY",
"MMDDYY"
];
const sanitizeInput = (input) =>
input
.trim()
.toUpperCase()
.replace(/\s*(am|pm)\s*/i, " $1")
.replaceAll(".", "/")
.replaceAll("-", "/");
export const fuzzyMatchDate = (dateString) => {
const sanitizedInput = sanitizeInput(dateString);
for (const format of dateFormats) {
const parsedDate = dayjs(sanitizedInput, format, true);
if (parsedDate.isValid()) {
return parsedDate;
}
}
for (const format of dateTimeFormats) {
const parsedDateTime = dayjs(sanitizedInput, format, true);
if (parsedDateTime.isValid()) {
return parsedDateTime; // Return the dayjs object
}
}
return null; // If no matching format is found
};

View File

@@ -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/handleBeta";
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";

View File

@@ -3,7 +3,7 @@ import React from "react";
import { useQuery } from "@apollo/client";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries";
import { QUERY_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_PARTS_BILLS_BY_JOBID, {
const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, {
variables: { jobid: job.id },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"

View File

@@ -2,30 +2,27 @@ 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 BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
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 FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
const { t } = useTranslation();
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
fetchPolicy: "network-only",
@@ -50,15 +47,9 @@ export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
children: (
<Row wrap>
<Col span={4}>
{!technician ? (
<>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
{line.parts_order.order_number}
</Link>
</>
) : (
`${line.parts_order.order_number}`
)}
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
{line.parts_order.order_number}
</Link>
</Col>
<Col span={4}>
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
@@ -93,17 +84,17 @@ export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
key: line.id,
children: (
<Row>
<Col span={8}>{line.parts_dispatch.number}</Col>
<Col span={8}>
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
</Col>
<Col span={8}>
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
</Col>
<Col span={8}>
{line.accepted_at ? (
<Space>
{t("parts_dispatch_lines.fields.accepted_at")}
<DateFormatter>{line.accepted_at}</DateFormatter>
</Space>
) : null}
<Space>
{t("parts_dispatch_lines.fields.accepted_at")}
<DateFormatter>{line.accepted_at}</DateFormatter>
</Space>
</Col>
</Row>
)
@@ -120,7 +111,6 @@ export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
<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
@@ -129,15 +119,9 @@ export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
children: (
<Row wrap>
<Col span={4}>
{!technician ? (
<>
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
{line.bill.invoice_number}
</Link>
</>
) : (
`${line.bill.invoice_number}`
)}
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
{line.bill.invoice_number}
</Link>
</Col>
<Col span={4}>
<span>

View File

@@ -3,21 +3,13 @@ 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";
const mapStateToProps = createStructuredSelector({
technician: selectTechnician
});
const mapDispatchToProps = (dispatch) => ({});
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
export default function JobLinesPartPriceChange({ job, line, refetch }) {
const [loading, setLoading] = useState(false);
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
@@ -60,7 +52,7 @@ export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
}
};
const popcontent = !technician && InstanceRenderManager({
const popcontent = InstanceRenderManager({
imex: null,
rome: (
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
@@ -103,4 +95,3 @@ export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
</JobLineConvertToLabor>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesPartPriceChange);

View File

@@ -31,20 +31,19 @@ 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,
@@ -55,7 +54,6 @@ 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" }))
});
@@ -65,7 +63,6 @@ export function JobLinesComponent({
jobRO,
technician,
setPartsOrderContext,
setPartsReceiveContext,
loading,
refetch,
jobLines,
@@ -74,11 +71,7 @@ export function JobLinesComponent({
setJobLineEditContext,
form,
setBillEnterContext,
setTaskUpsertContext,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick
setTaskUpsertContext
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
const {
@@ -205,6 +198,7 @@ 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",
@@ -219,6 +213,7 @@ export function JobLinesComponent({
dataIndex: "part_qty",
key: "part_qty"
},
// {
// title: t('joblines.fields.tax_part'),
// dataIndex: 'tax_part',
@@ -327,7 +322,7 @@ export function JobLinesComponent({
key: "actions",
render: (text, record) => (
<Space>
{(record.manual_line || jobIsPrivate) && !technician && (
{(record.manual_line || jobIsPrivate) && (
<>
<Button
disabled={jobRO}
@@ -342,6 +337,7 @@ export function JobLinesComponent({
</Button>
</>
)}
<Button
title={t("tasks.buttons.create")}
onClick={() => {
@@ -355,7 +351,7 @@ export function JobLinesComponent({
>
<FaTasks />
</Button>
{(record.manual_line || jobIsPrivate) && !technician && (
{(record.manual_line || jobIsPrivate) && (
<>
<Button
disabled={jobRO}
@@ -441,15 +437,6 @@ 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={
@@ -566,7 +553,7 @@ export function JobLinesComponent({
>
{t("joblines.actions.new")}
</Button>
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} /> })}
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
<Input.Search
placeholder={t("general.labels.search")}

View File

@@ -1,17 +1,7 @@
import React, { useMemo, useState } from "react";
import JobLinesComponent from "./job-lines.component";
function JobLinesContainer({
job,
joblines,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick,
refetch,
form,
...rest
}) {
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
const [searchText, setSearchText] = useState("");
const jobLines = useMemo(() => {
@@ -32,19 +22,7 @@ function JobLinesContainer({
}, [joblines, searchText]);
return (
<div>
<JobLinesComponent
refetch={refetch}
jobLines={jobLines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
setSearchText={setSearchText}
job={job}
form={form}
/>
</div>
<JobLinesComponent refetch={refetch} jobLines={jobLines} setSearchText={setSearchText} job={job} form={form} />
);
}

View File

@@ -11,18 +11,17 @@ 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({
technician: selectTechnician
//currentUser: selectCurrentUser
});
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, technician, ...otherBtnProps }) {
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, ...otherBtnProps }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -166,7 +165,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
return (
<>
{children}
{jobline.act_price !== 0 && !technician && (
{jobline.act_price !== 0 && (
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
<Tooltip title={t("joblines.actions.converttolabor")}>
<Button

View File

@@ -10,8 +10,8 @@ import {
QUERY_SCOREBOARD_ENTRY,
UPDATE_SCOREBOARD_ENTRY
} from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps }) {
const { t } = useTranslation();
@@ -86,7 +86,7 @@ export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps })
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.bodyhrs")}

View File

@@ -3,7 +3,7 @@ import axios from "axios";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
export default function JobSendPartPriceChangeComponent({ job, disabled }) {
export default function JobSendPartPriceChangeComponent({ job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleClick = async () => {
@@ -24,7 +24,7 @@ export default function JobSendPartPriceChangeComponent({ job, disabled }) {
};
return (
<Button onClick={handleClick} loading={loading} disabled={disabled}>
<Button onClick={handleClick} loading={loading}>
{t("jobs.actions.sendpartspricechange")}
</Button>
);

View File

@@ -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 InstanceRenderManager from "../../utils/instanceRenderMgr";
import { alphaSort } from "../../utils/sorters";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
export default function JobTotalsTableLabor({ job }) {
const { t } = useTranslation();
@@ -56,49 +56,16 @@ export default function JobTotalsTableLabor({ job }) {
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
render: (text, record) => record.hours.toFixed(1)
},
...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"
})
{
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()
}
];
const handleTableChange = (pagination, filters, sorter) => {
@@ -124,16 +91,6 @@ 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>
@@ -165,29 +122,7 @@ 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>
{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.Cell align="right">{Dinero(job.job_totals.rates.mapa.total).toFormat()}</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
@@ -216,29 +151,7 @@ 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>
{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.Cell align="right">{Dinero(job.job_totals.rates.mash.total).toFormat()}</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell>
@@ -246,16 +159,6 @@ 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>

View File

@@ -5,6 +5,7 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -19,14 +20,7 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminDatesChange);
@@ -93,7 +87,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DateTimePicker format="MM/DD/YYYY" isDateOnly />
<FormDatePicker format="MM/DD/YYYY" />
</Form.Item>
<Form.Item label={t("jobs.fields.date_towin")} name="date_towin">
<DateTimePicker />

View File

@@ -3,6 +3,7 @@ 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";
@@ -23,8 +24,6 @@ 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";
@@ -33,6 +32,7 @@ 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,13 +580,12 @@ function ResolveCCCLineIssues(estData, bodyshop) {
InstanceRenderManager({
executeFunction: true,
args: [],
rome: () => {
promanager: () => {
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"
}
});
});

View File

@@ -1,9 +1,18 @@
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import {
Collapse,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -20,7 +29,6 @@ import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.c
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -53,7 +61,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
<Form.Item
label={t("jobs.fields.regie_number")}
name="regie_number"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
@@ -105,7 +116,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />

View File

@@ -2,9 +2,9 @@ import { Form, Input } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function JobsCreateVehicleInfoNewComponent({ form }) {
const [state] = useContext(JobCreateContext);
@@ -113,7 +113,7 @@ export default function JobsCreateVehicleInfoNewComponent({ form }) {
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name={["vehicle", "data", "v_prod_dt"]}>
<DateTimePicker isDateOnly disabled={!state.vehicle.new} />
<FormDatePicker disabled={!state.vehicle.new} />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormRow from "../layout-form-row/layout-form-row.component";
@@ -29,7 +30,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<div>
<FormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DateTimePicker disabled={jobRO} isDateOnly />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker disabled={jobRO} />
@@ -44,7 +45,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<FormRow header={t("jobs.forms.scheddates")}>
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
<DateTimePicker disabled={jobRO} isDateOnly />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Tooltip title={t("jobs.labels.scheduledinchange")}>
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
@@ -84,6 +85,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
rules={[
{
required: jobInPostProduction
//message: t("general.validation.required"),
}
]}
>

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,7 +13,6 @@ import Car from "../job-damage-visual/job-damage-visual.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import FormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -152,7 +152,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Input disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DateTimePicker isDateOnly disabled={jobRO} />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use">
<Input disabled={jobRO} />

View File

@@ -1,13 +1,55 @@
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,
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
handlePartsDispatchOnRowClick
}) {
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) });
}
};
return (
<JobsDetailPliComponent
job={job}

View File

@@ -1,4 +1,4 @@
import { Collapse, Form, InputNumber, Switch } from "antd";
import { Collapse, Form, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -17,9 +17,6 @@ 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"]}
@@ -27,24 +24,6 @@ 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"]}
@@ -82,9 +61,6 @@ 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"]}
@@ -92,24 +68,6 @@ 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"]}
@@ -147,9 +105,6 @@ 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"]}
@@ -157,24 +112,6 @@ 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"]}
@@ -212,9 +149,6 @@ 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"]}
@@ -222,24 +156,6 @@ 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"]}
@@ -277,9 +193,6 @@ 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"]}
@@ -287,24 +200,6 @@ 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"]}
@@ -342,9 +237,6 @@ 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"]}
@@ -352,24 +244,6 @@ 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"]}
@@ -407,9 +281,6 @@ 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"]}
@@ -417,24 +288,6 @@ 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"]}
@@ -472,9 +325,6 @@ 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"]}
@@ -482,24 +332,6 @@ 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"]}
@@ -537,9 +369,6 @@ 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"]}
@@ -547,24 +376,6 @@ 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"]}

View File

@@ -23,9 +23,7 @@ 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"]}
@@ -33,24 +31,6 @@ 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"]}
@@ -94,9 +74,7 @@ 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"]}
@@ -104,24 +82,6 @@ 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"]}

View File

@@ -4,7 +4,6 @@ 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({
@@ -989,24 +988,18 @@ 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>
{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_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>
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_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_lbr_rt")} name="tax_lbr_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
</Form.Item>

View File

@@ -11,7 +11,6 @@ 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";
@@ -38,10 +37,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: search?.search
? (a, b) =>
parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((b.ro_number || "0").replace(/\D/g, ""))
: true,
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) => (
<Link to={"/manage/jobs/" + record.id}>{record.ro_number || t("general.labels.na")}</Link>
@@ -53,6 +49,7 @@ 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 ? (
@@ -70,6 +67,7 @@ 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} />
},
@@ -77,6 +75,7 @@ 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} />
},
@@ -86,7 +85,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
key: "status",
ellipsis: true,
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
sorter: true, // (a, b) => alphaSort(a.status, b.status),
sortOrder: sortcolumn === "status" && sortorder,
render: (text, record) => {
return record.status || t("general.labels.na");
@@ -101,6 +100,7 @@ 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: search?.search ? (a, b) => alphaSort(a.plate_no, b.plate_no) : true,
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
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: search?.search ? (a, b) => alphaSort(a.clm_no, b.clm_no) : true,
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder: sortcolumn === "clm_no" && sortorder,
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
},
@@ -142,7 +142,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
sorter: true, //(a, b) => a.clm_total - b.clm_total,
sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => {
return record.clm_total ? (
@@ -156,6 +157,7 @@ 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>
},
{

View File

@@ -16,15 +16,18 @@ 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) => ({});
const mapDispatchToProps = (dispatch) => ({
setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps))
});
export function JobsList({ bodyshop }) {
export function JobsList({ bodyshop, setJoyRideSteps }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())

View File

@@ -1,105 +0,0 @@
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>
);
}

View File

@@ -23,7 +23,7 @@ export function PartnerPingComponent({ bodyshop }) {
// Execute the created function directly
checkPartnerStatus(bodyshop);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop?.id]);
}, [bodyshop]);
return <></>;
}

View File

@@ -8,8 +8,8 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_UPDATE_BO_ETA } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { CalendarFilled } from "@ant-design/icons";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -62,7 +62,7 @@ export function PartsOrderBackorderEta({
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.save")}

View File

@@ -7,7 +7,7 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
{t("parts_orders.actions.backordered")}

View File

@@ -1,387 +0,0 @@
import { DeleteFilled } 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 [billData, setBillData] = useState(null);
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
useEffect(() => {
const fetchData = async () => {
if (selectedPartsOrderRecord?.returnfrombill) {
try {
const { data } = await billQuery({
variables: { billid: selectedPartsOrderRecord.returnfrombill }
});
setBillData(data);
} catch (error) {
console.error("Error fetching bill data:", error);
}
} else setBillData(null);
};
fetchData();
}, [selectedPartsOrderRecord, billQuery]);
const recordActions = (record) => (
<Space direction="horizontal" wrap>
<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) => ({
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) => ({
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 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);

View File

@@ -1,23 +1,33 @@
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd";
import React, { useState } from "react";
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 { 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 FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
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 PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import { FaTasks } from "react-icons/fa";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -52,6 +62,19 @@ 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 });
@@ -60,17 +83,42 @@ 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);
}}
>
@@ -250,6 +298,154 @@ 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
@@ -280,13 +476,15 @@ export function PartsOrderListTableComponent({
}
>
<PartsReceiveModalContainer />
<PartsOrderDrawer
job={job}
billsQuery={billsQuery}
handleOnRowClick={handleOnRowClick}
setPartsReceiveContext={setPartsReceiveContext}
setTaskUpsertContext={setTaskUpsertContext}
/>
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
open={selectedpartsorder}
closable
width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
<Table
loading={billsQuery.loading}
scroll={{

View File

@@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -74,7 +74,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
]}
label={t("parts_orders.fields.deliver_by")}
>
<DateTimePicker isDateOnly onlyFuture />
<FormDatePicker onlyFuture />
</Form.Item>
{job && job.special_coverage_policy && (
<Tag color="tomato">

View File

@@ -90,7 +90,7 @@ export function BillMarkSelectedExported({
onConfirm={handleUpdate}
disabled={disabled}
>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
{t("bills.labels.markexported")}
</Button>
</Popconfirm>

View File

@@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerFormItem from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import PaymentFormTotalPayments from "./payment-form.totalpayments.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -77,7 +77,7 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
}
]}
>
<DateTimePicker isDateOnly disabled={disabled} />
<DatePickerFormItem disabled={disabled} />
</Form.Item>
</LayoutFormRow>

View File

@@ -90,7 +90,7 @@ export function PaymentMarkSelectedExported({
onConfirm={handleUpdate}
disabled={disabled}
>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)} type="primary" danger>
<Button loading={loading} disabled={disabled} onClick={() => setOpen(true)}>
{t("bills.labels.markexported")}
</Button>
</Popconfirm>

View File

@@ -1,31 +1,22 @@
import { Button, Input, Space, Spin } from "antd";
import React, { useState } from "react";
import { Input, Space, Spin } from "antd";
import React 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>
@@ -45,13 +36,6 @@ 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>
);
}

View File

@@ -22,7 +22,7 @@ const CardColorLegend = ({ bodyshop }) => {
});
return (
<Col>
<Col style={{ marginLeft: "15px" }}>
<Typography>{t("production.labels.legend")}</Typography>
<List
grid={{

View File

@@ -0,0 +1,255 @@
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>
);
}

View File

@@ -1,437 +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 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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</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>&nbsp;</span>}
</EllipsesToolTip>
</Col>
);
const SubletsComponent = ({ metadata, cardSettings }) =>
cardSettings?.sublets && (
<Col span={12}>
{metadata.subletLines ? (
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
) : (
<span>&nbsp;</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>&nbsp;</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>
);
}

View File

@@ -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 } from "antd";
import { Button, notification, Skeleton, Space, Statistic } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -15,14 +15,12 @@ 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-color-legend.component.jsx";
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
import "./production-board-kanban.styles.scss";
import { createBoardData } from "./production-board-kanban.utils.js";
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
import ProductionBoardKanbanSettings from "./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,9 +37,9 @@ const mapDispatchToProps = (dispatch) => ({
)
});
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings }) {
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
const [filter, setFilter] = useState(defaultFilters);
const [filter, setFilter] = useState({ search: "", employeeId: null });
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const [orientation, setOrientation] = useState("vertical");
@@ -59,12 +57,11 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
useEffect(() => {
setIsMoving(true);
const newBoardData = createBoardData({
statuses,
const newBoardData = createBoardData(
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
data,
filter,
cardSettings: associationSettings?.kanban_settings
});
filter
);
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
...lane,
@@ -79,7 +76,7 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
return prevBoardLanes;
});
setIsMoving(false);
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
}, [data, bodyshop.md_ro_statuses, filter]);
const getCardByID = useCallback((data, cardId) => {
for (const lane of data.lanes) {
@@ -103,28 +100,23 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
if (!targetLane || !sourceLane) {
setIsMoving(false);
console.error("Invalid source or destination lane");
return;
}
const sameColumnTransfer = source.droppableId === destination.droppableId;
const sourceCard = getCardByID(boardLanes, draggableId);
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
const movedCardWillBeLast = destination.index > targetLane.cards.length - 1;
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[source.index + 1];
const oldChildCard = sourceLane.cards[destination.index + 1];
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? source.index < destination.index
? destination.index + 1
: destination.index
? destination.index - destination.index > 0
? destination.index
: destination.index + 1
: destination.index
];
@@ -135,14 +127,12 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (newChildCard) {
} else if (!!newChildCard) {
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
} else {
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? draggableId : null;
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
@@ -179,17 +169,58 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
setIsMoving(false);
}
},
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
[boardLanes, client, getCardByID, insertAuditTrail, isMoving, t]
);
const cardSettings = useMemo(() => {
const kanbanSettings = associationSettings?.kanban_settings;
return mergeWithDefaults(kanbanSettings);
}, [associationSettings]);
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 handleSettingsChange = () => {
setFilter(defaultFilters);
};
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);
}, []);
if (loading) {
return <Skeleton active />;
@@ -199,8 +230,14 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
<div>
<IndefiniteLoading loading={isMoving} />
<PageHeader
title={cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
style={{ paddingInline: 0, paddingBlock: 0 }}
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>
}
extra={
<Space wrap>
<Button onClick={() => refetch && refetch()}>
@@ -211,23 +248,13 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
parentLoading={setLoading}
associationSettings={associationSettings}
onSettingsChange={handleSettingsChange}
bodyshop={bodyshop}
data={data}
/>
</Space>
}
/>
<NoteUpsertModal />
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
<ProductionListDetailComponent jobs={data} />
<Board
queryData={data}
data={boardLanes}
onDragEnd={onDragEnd}
orientation={orientation}
cardSettings={cardSettings}
/>
<Board data={boardLanes} onDragEnd={onDragEnd} orientation={orientation} cardSettings={cardSettings} />
</div>
);
}

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from "react";
import { useQuery, useSubscription } from "@apollo/client";
import React, { useEffect, useMemo } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
@@ -13,14 +13,6 @@ const mapStateToProps = createStructuredSelector({
});
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
const combinedStatuses = useMemo(
() => [
...bodyshop.md_ro_statuses.production_statuses,
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
],
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
);
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
pollInterval: 3600000,
fetchPolicy: "network-only",
@@ -28,22 +20,18 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
});
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
});
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
variables: { email: currentUser.email },
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
});
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
useEffect(() => {
if (updatedJobs && data) {
if (updatedJobs) {
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, data, refetch]);
}, [updatedJobs, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;
@@ -55,8 +43,6 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
data={data ? data.jobs : []}
refetch={refetch}
associationSettings={filteredAssociationSettings}
bodyshop={bodyshop}
statuses={combinedStatuses}
/>
);
}

View File

@@ -0,0 +1,173 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Col, Form, notification, Popover, Row, Checkbox, Radio, Input, Switch } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
export default function ProductionBoardKanbanSettings({ associationSettings, parentLoading }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [orientation, setOrientation] = useState(true);
const [compact, setCompact] = useState(false);
const [colored, setColored] = useState(false);
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
const { t } = useTranslation();
useEffect(() => {
if (associationSettings?.kanban_settings) {
const { orientation = true, compact = true, cardcolor = true } = associationSettings.kanban_settings;
form.setFieldsValue(associationSettings.kanban_settings);
setOrientation(orientation);
setCompact(compact);
setColored(cardcolor);
}
}, [form, associationSettings]);
const handleFinish = async (values) => {
setLoading(true);
parentLoading(true);
const result = await updateKbSettings({
variables: {
id: associationSettings?.id,
ks: { ...values, orientation, compact, cardcolor: colored }
}
});
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 handleCheckedChanges = (checked, callback) => {
callback(checked);
setHasChanges(true);
};
const cardStyle = { minWidth: "50vw", marginTop: 10 };
const renderSwitchItem = (name, checked, callback, labelKey, checkedChildrenKey, unCheckedChildrenKey) => (
<Col span={4} key={name}>
<Form.Item name={name} valuePropName="checked" label={t(labelKey)}>
<Switch
checkedChildren={t(checkedChildrenKey)}
unCheckedChildren={t(unCheckedChildrenKey)}
checked={checked}
onChange={(checked) => handleCheckedChanges(checked, callback)}
/>
</Form.Item>
</Col>
);
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]}>
{renderSwitchItem(
"orientation",
orientation,
setOrientation,
"production.labels.orientation",
"production.labels.vertical",
"production.labels.horizontal"
)}
<Col span={4}>
<Form.Item name="cardSize" label={t("production.labels.card_size")}>
<Radio.Group>
<Radio.Button value="compact">{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>
{renderSwitchItem(
"compact",
compact,
setCompact,
"production.labels.compact",
"production.labels.tall",
"production.labels.wide"
)}
{renderSwitchItem(
"cardcolor",
colored,
setColored,
"production.labels.cardcolor",
"production.labels.on",
"production.labels.off"
)}
</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()}
<Form.Item name="orientation" style={{ display: "none" }}>
<Input type="hidden" value={orientation} />
</Form.Item>
<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>
);
}

View File

@@ -1,215 +0,0 @@
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;

View File

@@ -1,107 +1,128 @@
// Function to sort an array of objects by parentId
import { groupBy } from "lodash";
const sortByParentId = (arr) => {
// return arr.reduce((accumulator, currentValue) => {
// //Find the parent item.
// let item = accumulator.find((x) => x.id === currentValue.kanbanparent);
// //Get index of parent item
// let index = accumulator.indexOf(item);
// index = index !== -1 ? index + 1 : 0;
// accumulator.splice(index, 0, currentValue);
// return accumulator;
// }, []);
let parentId = "-1";
const sortedList = [];
const byParentsIdsList = groupBy(arr, "kanbanparent");
const byParentsIdsList = groupBy(arr, "kanbanparent"); // Create a new array with objects indexed by parentId
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
while (byParentsIdsList[parentId]) {
sortedList.push(...byParentsIdsList[parentId]);
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id;
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id; //Grab the ID from the last one.
}
if (byParentsIdsList["null"]) {
sortedList.push(...byParentsIdsList["null"]);
}
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
// Ensure all items are included in the sorted list
//Validate that the 2 arrays are of the same length and no children are missing.
if (arr.length !== sortedList.length) {
arr.forEach((origItem) => {
if (!sortedList.some((s) => s.id === origItem.id)) {
arr.map((origItem) => {
if (!!!sortedList.find((s) => s.id === origItem.id)) {
sortedList.push(origItem);
console.log("DATA CONSISTENCY ERROR: ", origItem.ro_number);
}
return 1;
});
}
return sortedList;
};
// Function to create board data based on statuses and jobs, with optional filtering
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
const { search, employeeId, alert } = filter;
export const createBoardData = (AllStatuses, Jobs, filter) => {
const { search, employeeId } = filter;
const lanes = AllStatuses.map((s) => {
return {
id: s,
title: s,
cards: []
};
});
const lanes = statuses.map((status) => ({
id: status,
title: status,
cards: []
}));
const filteredJobs =
(search === "" || !search) && !employeeId
? Jobs
: Jobs.filter((j) => {
let include = false;
if (search && search !== "") {
include = CheckSearch(search, j);
}
let filteredJobs =
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
if (!!employeeId) {
include =
include ||
j.employee_body === employeeId ||
j.employee_prep === employeeId ||
j.employee_csr === employeeId ||
j.employee_refinish === employeeId;
}
// Filter jobs by selectedMdInsCos if it has values
if (cardSettings?.selectedMdInsCos?.length > 0) {
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
}
return include;
});
// 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}`)
);
}
const DataGroupedByStatus = groupBy(filteredJobs, (d) => d.status);
// Filter jobs by alert if alert filter is true
if (alert) {
filteredJobs = filteredJobs.filter((job) => job.production_vars?.alert);
}
const DataGroupedByStatus = groupBy(filteredJobs, "status");
Object.keys(DataGroupedByStatus).forEach((statusGroupKey) => {
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
try {
const lane = lanes.find((l) => l.id === statusGroupKey);
if (!lane) return;
if (!lane?.cards) return null;
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
const { id, title, description, due_date, ...metadata } = job;
return {
id,
title,
description,
label: due_date || "",
label: job.due_date || "",
metadata
};
});
} catch (error) {
console.error("Error while creating board card", error);
console.log("Error while creating board card", error);
}
return null;
});
return { lanes };
};
// Function to check if a job matches the search and/or employeeId filter
const checkFilter = (search, employeeId, job) => {
const lowerSearch = search?.toLowerCase() ?? "";
const matchesSearch =
lowerSearch &&
[
job.ro_number,
job.ownr_fn,
job.ownr_co_nm,
job.ownr_ln,
job.status,
job.v_make_desc,
job.v_model_desc,
job.clm_no,
job.plate_no
].some((field) => field?.toLowerCase().includes(lowerSearch));
const matchesEmployeeId =
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
return matchesSearch || matchesEmployeeId;
const CheckSearch = (search, job) => {
return (
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
);
};
// export const updateBoardOnMove = (board, card, source, destination) => {
// //Slice from source
// const sourceCardList = board.columns.find((x) => x.id === source.fromColumnId)
// .cards;
// sourceCardList.slice(source.fromPosition, 0);
// //Splice into destination.
// const destCardList = board.columns.find(
// (x) => x.id === destination.toColumnId
// ).cards;
// console.log("updateBoardOnMove -> destCardList", destCardList);
// destCardList.splice(destination.toPosition, 0, card);
// console.log("updateBoardOnMove -> destCardList", destCardList);
// console.log("board", board);
// return board;
// };

View File

@@ -1,81 +0,0 @@
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;

View File

@@ -1,38 +0,0 @@
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;

View File

@@ -1,71 +0,0 @@
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;

View File

@@ -1,59 +0,0 @@
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;

View File

@@ -1,69 +0,0 @@
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 };

View File

@@ -1,169 +0,0 @@
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;

View File

@@ -14,7 +14,6 @@ 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;
@@ -32,30 +31,16 @@ 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, devicePixelRatio]);
}, [itemKey, setMaxHeight]);
useEffect(() => {
if (itemKey && heightMapRef.current.has(itemKey)) {

View File

@@ -7,7 +7,6 @@ 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());
@@ -31,8 +30,7 @@ const BoardContainer = ({
orientation = "horizontal",
cardSettings = {},
eventBusHandle,
reducerData,
queryData
reducerData
}) => {
const [isDragging, setIsDragging] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
@@ -126,36 +124,33 @@ const BoardContainer = ({
);
return (
<div>
<ProductionStatistics data={queryData} reducerData={currentReducerData} cardSettings={cardSettings} />
<PopoverWrapper>
<BoardWrapper orientation={orientation}>
<DragDropContext onDragEnd={onLaneDrag} onDragStart={onDragStart} contextId="production-board">
{currentReducerData.lanes.map((lane, index) => (
<Lane
key={lane.id}
id={lane.id}
title={lane.title}
index={index}
laneSortFunction={laneSortFunction}
orientation={orientation}
cards={lane.cards}
isDragging={isDragging}
isProcessing={isProcessing}
cardSettings={cardSettings}
maxLaneHeight={maxLaneHeight}
setMaxLaneHeight={setMaxLaneHeight}
maxCardHeight={maxCardHeight}
setMaxCardHeight={setMaxCardHeight}
maxCardWidth={maxCardWidth}
setMaxCardWidth={setMaxCardWidth}
lastDrag={getLastDragTime(lane.id)}
/>
))}
</DragDropContext>
</BoardWrapper>
</PopoverWrapper>
</div>
<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>
);
};

View File

@@ -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.component.jsx";
import ProductionBoardCard from "../../../production-board-kanban-card/production-board-kanban-card.component.jsx";
import HeightMemoryWrapper from "../components/HeightMemoryWrapper.jsx";
import SizeMemoryWrapper from "../components/SizeMemoryWrapper.jsx";
import ListComponent from "../components/ListComponent.jsx";

View File

@@ -2,6 +2,7 @@ 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";
@@ -9,23 +10,16 @@ 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) => ({
// Add any necessary dispatch actions here
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(ProductionColumnsComponent);
export function ProductionColumnsComponent({
columnState,
technician,
bodyshop,
data,
tableState,
refetch,
onColumnAdd
}) {
export function ProductionColumnsComponent({ columnState, technician, bodyshop, data, tableState, refetch }) {
const [columns, setColumns] = columnState;
const { t } = useTranslation();
const {
@@ -35,26 +29,18 @@ export function ProductionColumnsComponent({
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const handleAdd = (e) => {
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);
}
}
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 columnKeys = columns.map((i) => i.key);
@@ -90,4 +76,12 @@ export function ProductionColumnsComponent({
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductionColumnsComponent);
// <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)));
// }}
// />

View File

@@ -1,6 +1,6 @@
import { ExclamationCircleFilled, PlusCircleFilled } from "@ant-design/icons";
import { ExclamationCircleFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Popconfirm } from "antd";
import { Button } from "antd";
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -8,7 +8,6 @@ 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({});
@@ -23,24 +22,22 @@ const mapDispatchToProps = (dispatch) => ({
)
});
const ProductionListColumnAlert = ({ id, productionVars, refetch, insertAuditTrail }) => {
const ProductionListColumnAlert = ({ record, insertAuditTrail }) => {
const [updateAlert] = useMutation(UPDATE_JOB);
const { t } = useTranslation();
const handleAlertToggle = useCallback(() => {
logImEXEvent("production_toggle_alert");
const newAlertState = !!productionVars?.alert ? !productionVars?.alert : true;
const finalProductionVars = {
...productionVars,
alert: newAlertState
};
const newAlertState = !!record.production_vars?.alert ? !record.production_vars.alert : true;
updateAlert({
variables: {
jobId: id,
jobId: record.id,
job: {
production_vars: finalProductionVars
production_vars: {
...record.production_vars,
alert: newAlertState
}
}
}
}).catch((err) => {
@@ -48,26 +45,17 @@ const ProductionListColumnAlert = ({ id, productionVars, refetch, insertAuditTra
});
insertAuditTrail({
jobid: id,
jobid: record.id,
operation: AuditTrailMapping.alertToggle(newAlertState),
type: "alertToggle"
});
if (refetch) refetch();
}, [updateAlert, insertAuditTrail, id, productionVars, refetch]);
if (record.refetch) record.refetch();
}, [updateAlert, insertAuditTrail, record]);
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} />
);
if (!record.production_vars?.alert) return null;
return <Button className="production-alert" icon={<ExclamationCircleFilled />} onClick={handleAlertToggle} />;
};
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListColumnAlert);

View File

@@ -2,13 +2,10 @@ 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";
@@ -26,12 +23,10 @@ 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";
const getEmployeeName = (employeeId, employees) => {
const employee = employees.find((e) => e.id === employeeId);
return employee ? `${employee.first_name} ${employee.last_name}` : "";
};
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";
const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const { Enhanced_Payroll } = treatments;
@@ -263,7 +258,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
{ text: "True", value: true },
{ text: "False", value: false }
],
onFilter: (value, record) => value === record.special_coverage_policy,
onFilter: (value, record) => value.includes(record.special_coverage_policy),
render: (text, record) => <Checkbox checked={record.special_coverage_policy} />
},
@@ -354,14 +349,7 @@ 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,
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} />
)
render: (text, record) => <ProductionListColumnAlert record={{ record }} />
},
{
title: i18n.t("production.labels.note"),
@@ -382,7 +370,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} />;
}
},
{
@@ -431,8 +419,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_body" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_body, bodyshop.employees),
getEmployeeName(b.employee_body, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_body" />
@@ -445,8 +433,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_prep" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_prep, bodyshop.employees),
getEmployeeName(b.employee_prep, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} refetch={refetch} type="employee_prep" />
@@ -465,8 +453,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_csr, bodyshop.employees),
getEmployeeName(b.employee_csr, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_csr" />
@@ -479,8 +467,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_refinish" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_refinish, bodyshop.employees),
getEmployeeName(b.employee_refinish, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_refinish)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} refetch={refetch} type="employee_refinish" />

View File

@@ -1,12 +1,12 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Dropdown, Space } from "antd";
import { Button, Card, Dropdown, TimePicker } from "antd";
import dayjs from "../../utils/day";
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 DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function ProductionListDate({ record, field, time, pastIndicator }) {
const [updateAlert] = useMutation(UPDATE_JOB);
@@ -56,17 +56,23 @@ export default function ProductionListDate({ record, field, time, pastIndicator
key: "overlayItem1",
label: (
<Card style={{ padding: "1rem" }} onClick={(e) => e.stopPropagation()}>
<Space direction={"vertical"}>
<DateTimePicker
<FormDatePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && dayjs(record[field])) || null}
onChange={handleChange}
format="MM/DD/YYYY"
isDateOnly={!time}
/>
{time && (
<TimePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && dayjs(record[field])) || null}
onChange={handleChange}
format={time ? "MM/DD/YYYY hh:mm a" : "MM/DD/YYYY"}
isDateOnly={!time}
showTime={time ? { format: "hh:mm a", minuteStep: 15 } : false}
minuteStep={15}
format="hh:mm a"
/>
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
)}
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Card>
)
}

View File

@@ -148,7 +148,6 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
) : (
<PlusCircleFilled
style={iconStyle}
className="muted-button"
onClick={() => {
setAssignment({ operation: type });
setVisibility(true);

View File

@@ -0,0 +1,89 @@
import { useMutation } from "@apollo/client";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
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";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ProductionListSaveConfigButton({ columns, bodyshop, tableState }) {
const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [form] = Form.useForm();
const { t } = useTranslation();
const handleSaveConfig = async (values) => {
logImEXEvent("production_save_config");
setLoading(true);
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [
...bodyshop.production_config.filter((b) => b.name !== values.name),
//Assign it to the name
{
name: values.name,
columns: {
columnKeys: columns.map((i) => {
return { key: i.key, width: i.width };
}),
tableState
}
}
]
}
}
});
if (!!!result.errors) {
notification["success"]({ message: t("bodyshop.successes.save") });
} else {
notification["error"]({
message: t("bodyshop.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
form.resetFields();
setOpen(false);
setLoading(false);
};
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleSaveConfig}>
<Form.Item label={t("production.labels.viewname")} name="name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Space wrap>
<Button type="primary" danger onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
</Form>
</div>
);
return (
<Popover open={open} content={popMenu}>
<Button loading={loading} onClick={() => setOpen(true)}>
{t("production.actions.saveconfig")}
</Button>
</Popover>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListSaveConfigButton);

View File

@@ -1,505 +0,0 @@
import { DeleteOutlined, ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash";
const { confirm } = Modal;
export function ProductionListConfigManager({
refetch,
bodyshop,
technician,
currentUser,
state,
data,
columns,
setColumns,
setState,
onSave,
hasUnsavedChanges,
setHasUnsavedChanges
}) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [isAddingNewProfile, setIsAddingNewProfile] = useState(false);
const [form] = Form.useForm();
const [activeView, setActiveView] = useState(() => {
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
return assoc && assoc.default_prod_list_view;
});
const defaultState = {
sortedInfo: {
columnKey: "ro_number",
order: null
},
filteredInfo: {}
};
const ensureDefaultState = (state) => {
return {
sortedInfo: state?.sortedInfo || defaultState.sortedInfo,
filteredInfo: state?.filteredInfo || defaultState.filteredInfo,
...state
};
};
const createDefaultView = async () => {
const defaultConfig = {
name: t("production.constants.main_profile"),
columns: {
columnKeys: [
{ key: "ro_number", width: 100 },
{ key: "ownr", width: 100 },
{ key: "vehicle", width: 100 },
{ key: "ins_co_nm", width: 100 },
{ key: "actual_in", width: 100 },
{ key: "scheduled_completion", width: 100 },
{ key: "labhrs", width: 100 },
{ key: "employee_body", width: 100 },
{ key: "larhrs", width: 100 },
{ key: "employee_refinish", width: 100 },
{ key: "tt", width: 100 },
{ key: "status", width: 100 },
{ key: "sublets", width: 100 },
{ key: "viewdetail", width: 100 }
],
tableState: ensureDefaultState(state)
}
};
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [defaultConfig]
}
}
});
if (!result.errors) {
await updateActiveProdView(t("production.constants.main_profile"));
window.location.reload(); // Reload the page
} else {
notification.error({
message: t("bodyshop.errors.creatingdefaultview", {
error: JSON.stringify(result.errors)
})
});
}
};
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const updateActiveProdView = async (viewName) => {
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: viewName },
update(cache) {
cache.modify({
id: cache.identify(bodyshop),
fields: {
associations(existingAssociations) {
return existingAssociations.map((a) => {
if (a.useremail !== currentUser.email) return a;
return { ...a, default_prod_list_view: viewName };
});
}
}
});
}
});
setActiveView(viewName);
setHasUnsavedChanges(false);
}
};
const handleSelect = async (value) => {
if (hasUnsavedChanges) {
confirm({
title: t("general.labels.unsavedchanges"),
icon: <ExclamationCircleOutlined />,
content: t("general.messages.unsavedchangespopup"),
onOk: () => proceedWithSelect(value),
onCancel() {
// Do nothing if canceled
}
});
} else {
await proceedWithSelect(value);
}
};
const proceedWithSelect = async (value) => {
if (value === "add_new") {
setIsAddingNewProfile(true);
setOpen(true);
return;
}
const selectedConfig = bodyshop.production_config.find((pc) => pc.name === value);
// If the selected profile doesn't exist, revert to the main profile
if (!selectedConfig) {
const mainProfileConfig = bodyshop.production_config.find(
(pc) => pc.name === t("production.constants.main_profile")
);
if (mainProfileConfig) {
await updateActiveProdView(t("production.constants.main_profile"));
setColumns(
mainProfileConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
const newState = ensureDefaultState(mainProfileConfig.columns.tableState);
setState(newState);
if (onSave && isFunction(onSave)) {
onSave();
}
return;
}
}
// If the selected profile exists, proceed as normal
if (selectedConfig) {
const newColumns = selectedConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(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 = ensureDefaultState(selectedConfig.columns.tableState);
setState(newState);
await updateActiveProdView(value);
if (onSave && isFunction(onSave)) {
onSave();
}
}
};
const handleTrash = async (name) => {
if (name === t("production.constants.main_profile")) return;
const remainingConfigs = bodyshop.production_config.filter((b) => b.name !== name);
await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: remainingConfigs
}
},
awaitRefetchQueries: true
});
if (name === activeView) {
// Only switch profiles if the deleted profile was the active profile
if (remainingConfigs.length > 0) {
const nextConfig = remainingConfigs[0];
await updateActiveProdView(nextConfig.name);
setColumns(
nextConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state: ensureDefaultState(state),
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(nextConfig.columns.tableState));
} else {
await updateActiveProdView(null);
setColumns([]);
setState(defaultState);
}
} else {
// Revert back to the active view and load its columns and state
const activeConfig = bodyshop.production_config.find((pc) => pc.name === activeView);
if (activeConfig) {
await updateActiveProdView(activeView);
setColumns(
activeConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state: ensureDefaultState(state),
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(activeConfig.columns.tableState));
}
}
};
const handleSaveConfig = async (values) => {
logImEXEvent("production_save_config");
setLoading(true);
const profileName = isAddingNewProfile ? values.name : activeView;
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [
...bodyshop.production_config.filter((b) => b.name !== profileName),
{
name: profileName,
columns: {
columnKeys: columns.map((i) => ({ key: i.key, width: i.width })),
tableState: ensureDefaultState(state)
}
}
]
}
}
});
if (!result.errors) {
notification.success({ message: t("bodyshop.successes.save") });
if (isAddingNewProfile) {
await updateActiveProdView(profileName);
}
if (onSave && isFunction(onSave)) {
onSave();
}
setHasUnsavedChanges(false);
} else {
notification.error({
message: t("bodyshop.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
form.resetFields();
setOpen(false);
setLoading(false);
setIsAddingNewProfile(false);
};
useEffect(() => {
const validateAndSetDefaultView = () => {
const configExists = bodyshop.production_config.some((pc) => pc.name === activeView);
if (!configExists) {
// If the default view doesn't exist, revert to the main profile
const mainProfileConfig = bodyshop.production_config.find(
(pc) => pc.name === t("production.constants.main_profile")
);
if (mainProfileConfig) {
setActiveView(t("production.constants.main_profile"));
setColumns(
mainProfileConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(mainProfileConfig.columns.tableState));
updateActiveProdView(t("production.constants.main_profile"));
}
} else {
// If the default view exists, set it as active
setActiveView(activeView);
}
};
if (!bodyshop.production_config || bodyshop.production_config.length === 0) {
createDefaultView().catch((e) => {
console.error("Something went wrong saving the production list view Config.");
});
} else {
validateAndSetDefaultView();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeView, bodyshop.production_config]);
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleSaveConfig}>
{isAddingNewProfile && (
<Form.Item
label={t("production.labels.viewname")}
name="name"
rules={[
{ required: true, message: t("production.errors.name_required") },
{
validator: (_, value) => {
if (!value) {
return Promise.resolve();
}
const nameExists = bodyshop.production_config.some((pc) => pc.name === value);
if (nameExists) {
return Promise.reject(new Error(t("production.errors.name_exists")));
}
return Promise.resolve();
}
}
]}
>
<Input />
</Form.Item>
)}
<Space wrap>
<Button
type="primary"
danger
onClick={() => form.submit()}
loading={loading}
disabled={form.getFieldsError().some(({ errors }) => errors.length)}
>
{t("general.actions.save")}
</Button>
{!isAddingNewProfile && (
<Button
type="default"
onClick={() => {
setIsAddingNewProfile(true);
setOpen(true);
}}
>
{t("general.actions.saveas")}
</Button>
)}
<Button
onClick={() => {
setIsAddingNewProfile(false);
setOpen(false);
}}
>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
);
return (
<Space>
<Button loading={loading} onClick={() => setOpen(true)} disabled={isAddingNewProfile || !hasUnsavedChanges}>
{t("production.actions.saveconfig")}
</Button>
<Popover open={open} content={popMenu} placement="bottom">
<Select
style={{
minWidth: "150px"
}}
onSelect={handleSelect}
placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false}
value={activeView}
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
>
{bodyshop.production_config
.slice()
.sort((a, b) =>
a.name === t("production.constants.main_profile")
? -1
: b.name === t("production.constants.main_profile")
? 1
: 0
) //
.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
{config.name !== t("production.constants.main_profile") && (
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
onCancel={(e) => e.stopPropagation()}
>
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Popconfirm>
)}
</div>
</Select.Option>
))}
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
<div style={{ display: "flex", alignItems: "center" }}>
<PlusOutlined style={{ marginRight: "0.5rem" }} />
{t("production.labels.addnewprofile")}
</div>
</Select.Option>
</Select>
</Popover>
</Space>
);
}

View File

@@ -0,0 +1,157 @@
import { DeleteOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Popconfirm, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
currentUser: selectCurrentUser
});
export function ProductionListTable({ refetch, bodyshop, technician, currentUser, state, data, setColumns, setState }) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP);
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
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 assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: value },
update(cache) {
cache.modify({
id: cache.identify(bodyshop),
fields: {
associations(existingAssociations, { readField }) {
return existingAssociations.map((a) => {
if (a.useremail !== currentUser.email) return a;
return { ...a, default_prod_list_view: value };
});
}
}
});
}
});
}
};
const handleTrash = async (name) => {
await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: bodyshop.production_config.filter((b) => b.name !== name)
}
},
awaitRefetchQueries: true
});
setColumns(
bodyshop.production_config[0].columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state,
refetch,
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[0].columns.tableState);
};
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc && assoc.default_prod_list_view;
return (
<div style={{ width: "10rem" }}>
<Select
onSelect={handleSelect}
placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false}
defaultValue={defaultView}
>
{bodyshop.production_config.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}
>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
>
<DeleteOutlined
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
</div>
</Select.Option>
))}
</Select>
</div>
);
}
export default connect(mapStateToProps, null)(ProductionListTable);

View File

@@ -1,6 +1,8 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { SyncOutlined } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-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";
@@ -10,14 +12,10 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import ProductionListDetail from "../production-list-detail/production-list-detail.component";
import ProductionListSaveConfigButton from "../production-list-save-config-button/production-list-save-config-button.component";
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";
import { ProductionListConfigManager } from "./production-list-config-manager.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -27,7 +25,6 @@ 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 }
@@ -38,9 +35,10 @@ 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 initialStateRef = useRef(
const [state, setState] = useState(
(bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
bodyshop.production_config[0]?.columns.tableState || {
@@ -49,102 +47,80 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
}
);
const initialColumnsRef = useRef(
(initialStateRef.current &&
bodyshop.production_config
.find((p) => p.name === defaultView)
?.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
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
};
})) ||
[]
);
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) => {
const [columns, setColumns] = useState(
(state &&
matchingColumnConfig &&
matchingColumnConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
technician,
refetch,
technician,
state,
data: 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);
}
useEffect(() => {
const newColumns =
(state &&
matchingColumnConfig &&
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
};
})) ||
[];
setColumns(newColumns);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
//state,
matchingColumnConfig,
bodyshop,
technician,
data,
Enhanced_Payroll,
Production_List_Status_Colors,
refetch,
state,
columns
]);
data
]); //State removed from dependency array as it causes race condition when removing columns from table view and is not needed.
const handleTableChange = (pagination, filters, sorter) => {
const newState = {
setState({
...state,
filteredInfo: filters,
sortedInfo: { columnKey: sorter.columnKey, order: sorter.order }
};
if (!_.isEqual(newState, state)) {
setState(newState);
setHasUnsavedChanges(true);
}
});
};
const onDragEnd = (fromIndex, toIndex) => {
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 columnsCopy = columns.slice();
const item = columnsCopy.splice(fromIndex, 1)[0];
columnsCopy.splice(toIndex, 0, item);
setColumns(columnsCopy);
};
const removeColumn = (e) => {
const { key } = e;
const newColumns = columns.filter((i) => i.key !== key);
if (!_.isEqual(newColumns, columns)) {
setColumns(newColumns);
setHasUnsavedChanges(true);
}
setColumns(newColumns);
};
const handleResize =
@@ -155,21 +131,9 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
...nextColumns[index],
width: size.width
};
if (!_.isEqual(nextColumns, columns)) {
setColumns(nextColumns);
setHasUnsavedChanges(true);
}
setColumns(nextColumns);
};
const addColumn = (newColumn) => {
const updatedColumns = [...columns, newColumn];
if (!_.isEqual(updatedColumns, columns)) {
setColumns(updatedColumns);
setHasUnsavedChanges(true);
}
};
const headerItem = (col) => {
const menu = {
onClick: removeColumn,
@@ -188,29 +152,29 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
);
};
const resetChanges = () => {
setState(initialStateRef.current);
setColumns(initialColumnsRef.current);
setHasUnsavedChanges(false);
};
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 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));
// const handleSelectRecord = (record) => {
// if (selected !== record.id) {
// setSelected(record.id);
// } else {
// setSelected(null);
// }
// };
if (!!!columns) return <div>No columns found.</div>;
@@ -222,29 +186,8 @@ 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>
@@ -256,37 +199,20 @@ 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}
onColumnAdd={addColumn}
/>
<ProductionListColumnsAdd columnState={[columns, setColumns]} tableState={state} data={data} />
<ProductionListSaveConfigButton columns={columns} tableState={state} />
<ProductionListConfigManager
columns={columns}
setColumns={setColumns}
<ProductionListTableViewSelect
state={state}
setState={setState}
setColumns={setColumns}
refetch={refetch}
data={data}
bodyshop={bodyshop}
technician={technician}
currentUser={currentUser}
setHasUnsavedChanges={setHasUnsavedChanges}
hasUnsavedChanges={hasUnsavedChanges}
onSave={() => {
setHasUnsavedChanges(false);
initialStateRef.current = state;
}}
/>
<Input
onChange={(e) => setSearchText(e.target.value)}
placeholder={t("general.labels.search")}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { getOrderOperatorsByType, getWhereOperatorsByType } from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import { generateInternalReflections } from "./report-center-modal-utils";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({ form, bodyshop }) {
return (
@@ -196,8 +196,7 @@ function FiltersSection({ filters, form, bodyshop }) {
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<DateTimePicker
isDateOnly
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>

View File

@@ -4,7 +4,7 @@ import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function ScoreboardEntryEdit({ entry }) {
const [open, setOpen] = useState(false);
@@ -52,7 +52,7 @@ export default function ScoreboardEntryEdit({ entry }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.bodyhrs")}

View File

@@ -5,7 +5,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_VACATION } from "../../graphql/employees.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function ShopEmployeeAddVacation({ employee }) {
const { t } = useTranslation();
@@ -64,7 +64,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("employees.fields.vacation.end")}
@@ -90,7 +90,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
})
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Space wrap>

View File

@@ -21,12 +21,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
import queryString from "query-string";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -266,10 +266,10 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("employees.fields.user_email")}

View File

@@ -63,7 +63,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) {
value: false
}
],
onFilter: (value, record) => value === record.flat_rate,
onFilter: (value, record) => value === record.flate_rate,
render: (text, record) =>
record.flat_rate ? t("employees.labels.flat_rate") : t("employees.labels.straight_time")
},

View File

@@ -4,7 +4,6 @@ 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({
@@ -52,12 +51,6 @@ 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"]}
@@ -65,24 +58,6 @@ 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"]}
@@ -120,12 +95,6 @@ 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"]}
@@ -133,24 +102,6 @@ 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"]}
@@ -188,12 +139,6 @@ 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"]}
@@ -201,24 +146,6 @@ 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"]}
@@ -256,12 +183,6 @@ 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"]}
@@ -269,24 +190,6 @@ 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"]}
@@ -324,12 +227,6 @@ 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"]}
@@ -337,24 +234,6 @@ 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"]}
@@ -392,12 +271,6 @@ 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"]}
@@ -405,24 +278,6 @@ 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"]}
@@ -460,12 +315,6 @@ 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"]}
@@ -473,24 +322,6 @@ 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"]}
@@ -528,12 +359,6 @@ 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"]}
@@ -541,24 +366,6 @@ 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"]}
@@ -596,12 +403,6 @@ 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"]}
@@ -609,24 +410,6 @@ 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"]}
@@ -679,12 +462,7 @@ 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"]}
@@ -692,24 +470,6 @@ 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"]}
@@ -759,12 +519,7 @@ 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"]}
@@ -772,24 +527,6 @@ 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"]}
@@ -2028,37 +1765,25 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) {
</Form.Item>
</LayoutFormRow>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.tax_tow_rt")} name={["md_responsibility_centers", "tax_tow_rt"]}>
<Form.Item label={t("jobs.fields.tax_tow_rt")} name="tax_tow_rt">
<InputNumber min={0} max={100} precision={4} />
</Form.Item>
<Form.Item label={t("jobs.fields.tax_str_rt")} name={["md_responsibility_centers", "tax_str_rt"]}>
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
<InputNumber min={0} max={100} precision={4} />
</Form.Item>
{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"]}>
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
<InputNumber min={0} max={100} precision={4} />
</Form.Item>
{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"]}>
<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">
<InputNumber min={0} max={100} precision={4} />
</Form.Item>
</LayoutFormRow>

View File

@@ -1,13 +1,14 @@
import { Col, Form, Input, Row, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { FormDateTimePickerEnhanced } from "../form-date-time-picker-enhanced/form-date-time-picker-enhanced.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -245,8 +246,7 @@ export function TaskUpsertModalComponent({
</Col>
<Col span={8}>
<Form.Item label={t("tasks.fields.due_date")} name="due_date">
<DateTimePicker
isDateOnly
<FormDatePicker
onlyFuture
format="MM/DD/YYYY"
presets={generatePresets(selectedJobDetails)}
@@ -278,7 +278,12 @@ export function TaskUpsertModalComponent({
}
]}
>
<DateTimePicker onlyFuture presets={generatePresets(selectedJobDetails)} />
<FormDateTimePickerEnhanced
onlyFuture
showTime
minuteStep={15}
presets={generatePresets(selectedJobDetails)}
/>
</Form.Item>
</Col>
</Row>

View File

@@ -1,4 +1,5 @@
import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
import dayjs from "../../utils/day";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -6,12 +7,10 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges";
import dayjs from "../../utils/day";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectTechnician,
technician: selectTechnician
});
const mapDispatchToProps = (dispatch) => ({
@@ -19,7 +18,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(TechJobPrintTickets);
export function TechJobPrintTickets({ bodyshop, technician, event, attendacePrint }) {
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -58,8 +57,7 @@ export function TechJobPrintTickets({ bodyshop, technician, event, attendacePrin
subject:
attendacePrint === true ? Templates.attendance_employee.subject : Templates.timetickets_employee.subject
},
values.sendby,
bodyshop
values.sendby // === "email" ? "e" : "p"
);
} catch (error) {
console.log(error);

View File

@@ -7,9 +7,9 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>

View File

@@ -1,6 +1,6 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Checkbox, Space, Table } from "antd";
import dayjs from "../../utils/day";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,10 +10,10 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import dayjs from "../../utils/day";
import { alphaSort, dateSort } from "../../utils/sorters";
import RbacWrapper, { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -165,7 +165,7 @@ export function TimeTicketList({
key: "memo",
sorter: (a, b) => alphaSort(a.memo, b.memo),
sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
render: (text, record) => (record.memo?.startsWith("timetickets.labels") ? t(record.memo) : record.memo)
render: (text, record) => (record.clockon || record.clockoff ? t(record.memo) : record.memo)
},
...(Enhanced_Payroll.treatment === "on"
? [

View File

@@ -1,5 +1,4 @@
import { useLazyQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -8,10 +7,8 @@ import { createStructuredSelector } from "reselect";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import {
default as DateTimePicker,
default as FormDateTimePicker
} from "../form-date-time-picker/form-date-time-picker.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
@@ -19,6 +16,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -62,8 +60,8 @@ export function TimeTicketModalComponent({
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center}
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center}
</Select.Option>
))}
</Select>
@@ -71,7 +69,13 @@ export function TimeTicketModalComponent({
};
const MemoInput = ({ value, ...props }) => {
return <Input value={value?.startsWith("timetickets.labels") ? t(value) : value} {...props} />;
return (
<Input
value={value?.startsWith("timetickets.") ? t(value) : value}
{...props}
disabled={value?.startsWith("timetickets.") || disabled}
/>
);
};
return (
@@ -107,7 +111,7 @@ export function TimeTicketModalComponent({
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
name="employeeid"

View File

@@ -39,7 +39,7 @@ export default function TimeTicketShiftActive({ timetickets, refetch, isTechCons
renderItem={(ticket) => (
<List.Item>
<Card
title={ticket.memo?.startsWith("timetickets.labels") ? t(ticket.memo) : ticket.memo}
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}

View File

@@ -1,7 +1,7 @@
import { AlertOutlined } from "@ant-design/icons";
import { Alert, Button, Col, Row, Space } from "antd";
import i18n from "i18next";
import React, { useEffect } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -12,42 +12,39 @@ 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],
needRefresh: [needRefresh],
offlineReady: [
offlineReady //setOfflineReady
],
needRefresh: [
needRefresh //setNeedRefresh
],
updateServiceWorker
} = useRegisterSW({
onRegistered(r) {
console.log("SW Registered:", r);
if (r) {
setInterval(
() => {
r.update();
},
10 * 60 * 1000
);
}
// eslint-disable-next-line prefer-template
console.log("SW Registered: " + r);
r &&
setInterval(() => {
r.update();
}, intervalMS);
},
onRegisterError(error) {
console.error("SW registration error", error);
console.log("SW registration error", error);
}
});
useEffect(() => {
if (import.meta.env.DEV) {
console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
}
}, [needRefresh, offlineReady]);
if (import.meta.env.DEV) console.log(`SW Status => Refresh? ${needRefresh} - offlineReady? ${offlineReady}`);
if (!needRefresh) return null;
return (
<Alert
message={t("general.messages.newversiontitle", {
@@ -72,10 +69,19 @@ export function UpdateAlert({ updateAvailable }) {
</Col>
<Col sm={24} md={8} lg={6}>
<Space wrap>
<Button onClick={() => window.open("https://imex-online.noticeable.news/", "_blank")}>
<Button
onClick={async () => {
window.open("https://imex-online.noticeable.news/", "_blank");
}}
>
{i18n.t("general.actions.viewreleasenotes")}
</Button>
<Button type="primary" onClick={() => updateServiceWorker(true)}>
<Button
type="primary"
onClick={async () => {
updateServiceWorker(true);
}}
>
{i18n.t("general.actions.refresh")}
</Button>
</Space>
@@ -87,5 +93,3 @@ export function UpdateAlert({ updateAvailable }) {
/>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(UpdateAlert);

View File

@@ -1,9 +1,9 @@
import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function VehicleDetailFormComponent({ form, loading }) {
const { t } = useTranslation();
@@ -102,7 +102,7 @@ export default function VehicleDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name="v_prod_dt">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_paint_codes", { number: 1 })} name={["v_paint_codes", "paint_cd1"]}>

View File

@@ -50,7 +50,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
}
`;
export const QUERY_PARTS_BILLS_BY_JOBID = gql`
export const QUERY_BILLS_BY_JOBID = gql`
query QUERY_PARTS_BILLS_BY_JOBID($jobid: uuid!) {
parts_orders(where: { jobid: { _eq: $jobid } }, order_by: { order_date: desc }) {
id

View File

@@ -1,7 +1,7 @@
import { gql } from "@apollo/client";
export const QUERY_ALL_ACTIVE_JOBS_PAGINATED = gql`
query QUERY_ALL_ACTIVE_JOBS_PAGINATED(
query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED(
$offset: Int
$limit: Int
$order: [jobs_order_by!]
@@ -151,6 +151,14 @@ export const QUERY_PARTS_QUEUE = gql`
}
}
`;
export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
subscription SUBSCRIPTION_JOBS_IN_PRODUCTION {
jobs(where: { inproduction: { _eq: true } }) {
id
updated_at
}
}
`;
export const QUERY_EXACT_JOB_IN_PRODUCTION = gql`
query QUERY_EXACT_JOB_IN_PRODUCTION($id: uuid!) {
@@ -292,6 +300,82 @@ 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) {
@@ -820,7 +904,10 @@ export const QUERY_JOB_CARD_DETAILS = gql`
status
}
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
joblines(
where: { removed: { _eq: false } }
order_by: { line_no: asc }
) {
id
alt_partm
line_no
@@ -1062,7 +1149,6 @@ export const UPDATE_JOB = gql`
suspended
queued_for_parts
scheduled_completion
scheduled_delivery
actual_in
date_repairstarted
date_void
@@ -1942,6 +2028,10 @@ export const generate_UPDATE_JOB_KANBAN = (
newChildId,
newChildParent
) => {
// console.log("oldChildId", oldChildId, "oldChildNewParent", oldChildNewParent);
// console.log("Moved", movedId, movedNewParent, movedNewStatus);
// console.log("new", newChildId, newChildParent);
const oldChildQuery = `
updateOldChild: update_jobs(where: { id: { _eq: "${oldChildId}" } },
_set: {kanbanparent: ${oldChildNewParent ? `"${oldChildNewParent}"` : null}}) {
@@ -2452,94 +2542,3 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
}
}
`;
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
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
job_totals
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
}
}
}
`;

View File

@@ -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,6 +159,13 @@ 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,
@@ -254,34 +261,19 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
prt_tax_rt: bodyshop.bill_tax_rates.state_tax_rate / 100
}
},
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"
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
}
}
})
}}
>

View File

@@ -8,11 +8,11 @@ import Icon, {
SyncOutlined,
ToolFilled
} from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useQuery } from "@apollo/client";
import { Badge, Button, Divider, Form, notification, Space, Tabs } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import Axios from "axios";
import _ from "lodash";
import dayjs from "../../utils/day";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -20,13 +20,11 @@ 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";
@@ -44,18 +42,19 @@ 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 { DateTimeFormat } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
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 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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -96,11 +95,6 @@ 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));
@@ -109,42 +103,6 @@ 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);
@@ -344,18 +302,7 @@ export function JobsDetailPage({
id: "job-details-repairdata",
label: t("menus.jobsdetail.repairdata"),
forceRender: true,
children: (
<JobsLinesContainer
job={job}
joblines={job.joblines}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
refetch={refetch}
form={form}
/>
)
children: <JobsLinesContainer job={job} joblines={job.joblines} refetch={refetch} form={form} />
},
{
key: "rates",
@@ -379,15 +326,7 @@ export function JobsDetailPage({
label: HasFeatureAccess({ featureName: "bills", bodyshop })
? t("menus.jobsdetail.partssublet")
: t("menus.jobsdetail.parts"),
children: (
<JobsDetailPliContainer
job={job}
billsQuery={billsQuery}
handleBillOnRowClick={handleBillOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
/>
)
children: <JobsDetailPliContainer job={job} />
},
...(InstanceRenderManager({
imex: true,

View File

@@ -12,6 +12,7 @@ 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";
@@ -22,6 +23,8 @@ 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";
@@ -102,12 +105,16 @@ const { Content, Footer } = Layout;
const mapStateToProps = createStructuredSelector({
conflict: selectInstanceConflict,
bodyshop: selectBodyshop
bodyshop: selectBodyshop,
enableJoyRide: selectEnableJoyRide,
joyRideSteps: selectJoyRideSteps
});
const mapDispatchToProps = (dispatch) => ({});
const mapDispatchToProps = (dispatch) => ({
setJoyRideFinished: (steps) => dispatch(setJoyRideFinished(steps))
});
export function Manage({ conflict, bodyshop }) {
export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoyRideFinished }) {
const { t } = useTranslation();
const [chatVisible] = useState(false);
@@ -571,11 +578,26 @@ export function Manage({ conflict, bodyshop }) {
return (
<>
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
<ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
<Layout style={{ minHeight: "100vh" }} className="layout-container">
<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}

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