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

@@ -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

@@ -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")}

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

@@ -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

@@ -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

@@ -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

@@ -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,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

@@ -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,411 +0,0 @@
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { FaTasks } from "react-icons/fa";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import DataLabel from "../data-label/data-label.component";
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "billEnter"
})
),
setPartsReceiveContext: (context) =>
dispatch(
setModalContext({
context: context,
modal: "partsReceive"
})
),
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
});
export function PartsOrderListTableDrawerComponent({
setBillEnterContext,
bodyshop,
jobRO,
job,
billsQuery,
handleOnRowClick,
setPartsReceiveContext,
setTaskUpsertContext
}) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const bpoints = {
xs: "100%",
sm: "100%",
md: "100%",
lg: "75%",
xl: "75%",
xxl: "65%"
};
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
const responsibilityCenters = bodyshop.md_responsibility_centers;
const Templates = TemplateList("partsorder", { job });
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {}
});
const [returnfrombill, setReturnFromBill] = useState();
const [billData, setBillData] = useState();
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
useEffect(() => {
if (returnfrombill === null) {
setBillData(null);
} else {
const fetchData = async () => {
const result = await billQuery({
variables: { billid: returnfrombill }
});
setBillData(result.data);
};
fetchData();
}
}, [returnfrombill, billQuery]);
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
{showView && (
<Button
onClick={() => {
if (record.returnfrombill) {
setReturnFromBill(record.returnfrombill);
} else {
setReturnFromBill(null);
}
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
<Button
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setPartsReceiveContext({
actions: { refetch: refetch },
context: {
jobId: job.id,
job: job,
partsorderlines: record.parts_order_lines.map((pol) => {
return {
joblineid: pol.job_line_id,
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno
};
})
}
});
}}
>
{t("parts_orders.actions.receive")}
</Button>
<Button
title={t("tasks.buttons.create")}
onClick={() => {
setTaskUpsertContext({
context: {
jobid: job.id,
partsorderid: record.id
}
});
}}
>
<FaTasks />
</Button>
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={jobRO}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrder({
variables: { partsOrderId: record.id },
update(cache) {
cache.modify({
fields: {
parts_orders(existingPartsOrders, { readField }) {
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
}
}
});
}
});
}}
>
<Button disabled={jobRO}>
<DeleteFilled />
</Button>
</Popconfirm>
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
<Button
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setBillEnterContext({
actions: { refetch: refetch },
context: {
job: job,
bill: {
vendorid: record.vendor.id,
is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => {
return {
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
actual_price: pol.act_price,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
: null
};
})
}
}
});
}}
>
{t("parts_orders.actions.receivebill")}
</Button>
</FeatureWrapperComponent>
<PrintWrapper
templateObject={{
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
variables: { id: record.id }
}}
messageObject={{
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
to: record.vendor.email
}}
id={job.id}
/>
</Space>
);
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
},
{
title: t("parts_orders.fields.quantity"),
dataIndex: "quantity",
key: "quantity",
sorter: (a, b) => a.quantity - b.quantity,
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
},
{
title: t("parts_orders.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cost"),
dataIndex: "cost",
key: "cost",
sorter: (a, b) => a.cost - b.cost,
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
}
]
: []),
{
title: t("parts_orders.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
},
{
title: t("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks"
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status"
},
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
? [
{
title: t("parts_orders.fields.cm_received"),
dataIndex: "cm_received",
key: "cm_received",
render: (text, record) => (
<PartsOrderCmReceived
orderLineId={record.id}
checked={record.cm_received}
partsorderid={selectedPartsOrderRecord.id}
/>
)
}
]
: []),
{
title: t("parts_orders.fields.backordered_on"),
dataIndex: "backordered_on",
key: "backordered_on",
render: (text, record) => <DateFormatter>{text}</DateFormatter>
},
{
title: t("parts_orders.fields.backordered_eta"),
dataIndex: "backordered_eta",
key: "backordered_eta",
render: (text, record) => (
<PartsOrderBackorderEta
backordered_eta={record.backordered_eta}
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
)
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
)
}
];
return (
<div>
<PageHeader
title={
billData
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
: `${record.vendor.name} - ${record.order_number}`
}
extra={recordActions(record)}
/>
<Table
scroll={{
x: true //y: "50rem"
}}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
onChange={handleTableChange}
/>
<DataLabel label={t("parts_orders.fields.comments")}>
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
</DataLabel>
</div>
);
};
return (
<div>
<PartsReceiveModalContainer />
<Drawer
placement="right"
onClose={() => handleOnRowClick(null)}
open={selectedpartsorder}
closable
width={drawerPercentage}
>
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
</Drawer>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderListTableDrawerComponent);

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

@@ -17,6 +17,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
const { t } = useTranslation();
return (
<Space wrap>
{loading && <Spin />}

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 { mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -39,7 +37,7 @@ 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({ search: "", employeeId: null });
const [loading, setLoading] = useState(true);
@@ -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,13 +169,52 @@ 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 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);
@@ -201,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()}>
@@ -213,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,102 +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 }) => {
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);
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,67 +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 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 };

View File

@@ -1,162 +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";
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data }) {
const [form] = Form.useForm();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [statisticsOrder, setStatisticsOrder] = useState(defaultKanbanSettings.statisticsOrder);
const [selectedMdInsCos, setSelectedMdInsCos] = useState(defaultKanbanSettings.selectedMdInsCos);
const [selectedEstimators, setSelectedEstimators] = useState(defaultKanbanSettings.selectedEstimators);
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
const { t } = useTranslation();
useEffect(() => {
if (associationSettings?.kanban_settings) {
const finalSettings = mergeWithDefaults(associationSettings.kanban_settings);
form.setFieldsValue(finalSettings);
setStatisticsOrder(finalSettings.statisticsOrder);
setSelectedMdInsCos(finalSettings.selectedMdInsCos);
setSelectedEstimators(finalSettings.selectedEstimators);
}
}, [form, associationSettings]);
const handleFinish = async (values) => {
setLoading(true);
parentLoading(true);
const result = await updateKbSettings({
variables: {
id: associationSettings?.id,
ks: {
...associationSettings.kanban_settings,
...values,
statisticsOrder,
selectedMdInsCos,
selectedEstimators
}
}
});
if (result.errors) {
notification.open({
type: "error",
message: t("production.errors.settings", {
error: JSON.stringify(result.errors)
})
});
}
setOpen(false);
setLoading(false);
parentLoading(false);
setHasChanges(false);
};
const handleValuesChange = () => setHasChanges(true);
const handleRestoreDefaults = () => {
form.setFieldsValue({
...defaultKanbanSettings,
statisticsOrder: defaultKanbanSettings.statisticsOrder
});
setStatisticsOrder(defaultKanbanSettings.statisticsOrder);
setSelectedMdInsCos(defaultKanbanSettings.selectedMdInsCos);
setSelectedEstimators(defaultKanbanSettings.selectedEstimators);
setHasChanges(true);
};
const overlay = (
<Card style={{ minWidth: "80vw" }}>
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
<Tabs
defaultActiveKey="1"
items={[
{
key: "1",
label: t("production.settings.layout"),
children: <LayoutSettings t={t} />
},
{
key: "2",
label: t("production.settings.information"),
children: <InformationSettings t={t} />
},
{
key: "3",
label: t("production.settings.statistics_title"),
children: (
<StatisticsSettings
t={t}
statisticsOrder={statisticsOrder}
setStatisticsOrder={setStatisticsOrder}
setHasChanges={setHasChanges}
/>
)
},
{
key: "4",
label: t("production.settings.filters_title"),
children: (
<FilterSettings
selectedMdInsCos={selectedMdInsCos}
setSelectedMdInsCos={setSelectedMdInsCos}
selectedEstimators={selectedEstimators}
setSelectedEstimators={setSelectedEstimators}
setHasChanges={setHasChanges}
bodyshop={bodyshop}
data={data}
/>
)
}
]}
/>
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
<Col span={8}>
<Button block onClick={() => setOpen(false)}>
{t("general.actions.cancel")}
</Button>
</Col>
<Col span={8}>
<Button block onClick={handleRestoreDefaults}>
{t("general.actions.defaults")}
</Button>
</Col>
<Col span={8}>
<Button block onClick={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
{t("general.actions.save")}
</Button>
</Col>
</Row>
</Form>
</Card>
);
return (
<Popover content={overlay} open={open} placement="topRight">
<Button loading={loading} onClick={() => setOpen(!open)}>
{t("production.settings.board_settings")}
</Button>
</Popover>
);
}
ProductionBoardKanbanSettings.propTypes = {
associationSettings: PropTypes.object,
parentLoading: PropTypes.func.isRequired,
bodyshop: PropTypes.object.isRequired,
data: PropTypes.array
};
export default ProductionBoardKanbanSettings;

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

@@ -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

@@ -23,7 +23,7 @@ import ProductionListColumnPartsReceived from "./production-list-columns.partsre
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
import ProductionListColumnCategory from "./production-list-columns.status.category";
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
import { store } from "../../redux/store";
import { setModalContext } from "../../redux/modals/modals.actions";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
@@ -349,9 +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,
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"),
@@ -372,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} />;
}
},
{

View File

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

@@ -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,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

@@ -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

@@ -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);
@@ -576,6 +583,21 @@ export function Manage({ conflict, bodyshop }) {
<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}

View File

@@ -1,12 +1,6 @@
import React from "react";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
import ProductionListTable from "../../components/production-list-table/production-list-table.container";
export default function ProductionListComponent() {
return (
<>
<NoteUpsertModal />
<ProductionListTable />
</>
);
return <ProductionListTable />;
}

View File

@@ -33,8 +33,6 @@ const TimeTicketModalTask = lazy(
const TechAssignedProdJobs = lazy(() => import("../tech-assigned-prod-jobs/tech-assigned-prod-jobs.component"));
const TechDispatchedParts = lazy(() => import("../tech-dispatched-parts/tech-dispatched-parts.page"));
const TaskUpsertModalContainer = lazy(() => import("../../components/task-upsert-modal/task-upsert-modal.container"));
const { Content } = Layout;
const mapStateToProps = createStructuredSelector({
@@ -69,7 +67,6 @@ export function TechPage({ technician }) {
<UpdateAlert />
<TechHeader />
<TechLookupJobsDrawer />
<TaskUpsertModalContainer />
<Content className="tech-content-container">
<ErrorBoundary>
<Suspense

View File

@@ -67,3 +67,11 @@ export const setUpdateAvailable = (isUpdateAvailable) => ({
type: ApplicationActionTypes.SET_UPDATE_AVAILABLE,
payload: isUpdateAvailable
});
export const setJoyRideSteps = (steps) => ({
type: ApplicationActionTypes.SET_JOYRIDE_STEPS,
payload: steps
});
export const setJoyRideFinished = () => ({
type: ApplicationActionTypes.SET_JOYRIDE_FINISHED
//payload: isUpdateAvailable,
});

View File

@@ -14,7 +14,9 @@ const INITIAL_STATE = {
error: null
},
jobReadOnly: false,
partnerVersion: null
partnerVersion: null,
enableJoyRide: false,
joyRideSteps: []
};
const applicationReducer = (state = INITIAL_STATE, action) => {
@@ -87,6 +89,12 @@ const applicationReducer = (state = INITIAL_STATE, action) => {
case ApplicationActionTypes.SET_PROBLEM_JOBS: {
return { ...state, problemJobs: action.payload };
}
case ApplicationActionTypes.SET_JOYRIDE_STEPS: {
return { ...state, enableJoyRide: true, joyRideSteps: action.payload };
}
case ApplicationActionTypes.SET_JOYRIDE_FINISHED: {
return { ...state, enableJoyRide: false, joyRideSteps: [] };
}
default:
return state;
}

View File

@@ -22,3 +22,5 @@ export const selectJobReadOnly = createSelector([selectApplication], (applicatio
export const selectOnline = createSelector([selectApplication], (application) => application.online);
export const selectProblemJobs = createSelector([selectApplication], (application) => application.problemJobs);
export const selectUpdateAvailable = createSelector([selectApplication], (application) => application.updateAvailable);
export const selectEnableJoyRide = createSelector([selectApplication], (application) => application.enableJoyRide);
export const selectJoyRideSteps = createSelector([selectApplication], (application) => application.joyRideSteps);

View File

@@ -12,6 +12,8 @@ const ApplicationActionTypes = {
SET_ONLINE_STATUS: "SET_ONLINE_STATUS",
INSERT_AUDIT_TRAIL: "INSERT_AUDIT_TRAIL",
SET_PROBLEM_JOBS: "SET_PROBLEM_JOBS",
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE"
SET_UPDATE_AVAILABLE: "SET_UPDATE_AVAILABLE",
SET_JOYRIDE_STEPS: "SET_JOYRIDE_STEPS",
SET_JOYRIDE_FINISHED: "SET_JOYRIDE_FINISHED"
};
export default ApplicationActionTypes;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1194,17 +1194,6 @@ export const TemplateList = (type, context) => {
},
group: "customers"
},
payments_by_date_payment: {
title: i18n.t("reportcenter.templates.payments_by_date_payment"),
subject: i18n.t("reportcenter.templates.payments_by_date_payment"),
key: "payments_by_date_payment",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.payments"),
field: i18n.t("payments.fields.date")
},
group: "customers"
},
schedule: {
title: i18n.t("reportcenter.templates.schedule"),
subject: i18n.t("reportcenter.templates.schedule"),

View File

@@ -6,6 +6,8 @@ import * as url from "url";
import { defineConfig } from "vite";
import { ViteEjsPlugin } from "vite-plugin-ejs";
import eslint from "vite-plugin-eslint";
//import CompressionPlugin from 'vite-plugin-compression';
import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
@@ -14,8 +16,7 @@ process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
});
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
function reactVirtualizedFix() {
function reactVirtualized() {
return {
name: "flat:react-virtualized",
configResolved: async () => {
@@ -36,7 +37,10 @@ function reactVirtualizedFix() {
export default defineConfig({
base: "/",
plugins: [
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
ViteEjsPlugin((viteConfig) => {
// viteConfig is the current Vite resolved config
return { env: viteConfig.env };
}),
VitePWA({
injectRegister: "auto",
registerType: "prompt",
@@ -56,6 +60,7 @@ export default defineConfig({
description: "The ultimate bodyshop management system.",
icons: [
{
//TODO:AIO Ensure that these are correct for Rome and IO.
src: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE,
imex: "favicon.png",
@@ -96,7 +101,7 @@ export default defineConfig({
gcm_sender_id: "103953800507"
}
}),
reactVirtualizedFix(),
reactVirtualized(),
react(),
eslint()
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.

View File

@@ -4,5 +4,5 @@
include_in_metadata: true
payload: {}
headers:
- name: event-secret
value_from_env: EVENT_SECRET
- name: event-secret
value_from_env: EVENT_SECRET

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."jobs" add column "nextCardId" uuid
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."jobs" add column "nextCardId" uuid
null;

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "kanbanchild" to "nextCardId";

View File

@@ -0,0 +1 @@
alter table "public"."jobs" rename column "nextCardId" to "kanbanchild";

View File

@@ -1,9 +0,0 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- UPDATE "public"."masterdata"
-- SET value = jsonb_set(
-- value::jsonb,
-- '{OP20}',
-- '{"desc": "REMOVE AND REINSTALL", "opcode": "OP20", "partcode": "PAE"}'::jsonb,
-- true
-- );

View File

@@ -1,7 +0,0 @@
UPDATE "public"."masterdata"
SET value = jsonb_set(
value::jsonb,
'{OP20}',
'{"desc": "REMOVE AND REINSTALL", "opcode": "OP20", "partcode": "PAE"}'::jsonb,
true
);

613
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,18 +19,18 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.629.0",
"@aws-sdk/client-ses": "^3.629.0",
"@aws-sdk/credential-provider-node": "^3.629.0",
"@opensearch-project/opensearch": "^2.11.0",
"aws4": "^1.13.1",
"axios": "^1.7.4",
"@aws-sdk/client-secrets-manager": "^3.614.0",
"@aws-sdk/client-ses": "^3.614.0",
"@aws-sdk/credential-provider-node": "^3.614.0",
"@opensearch-project/opensearch": "^2.10.0",
"aws4": "^1.13.0",
"axios": "^1.7.2",
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"canvas": "^2.11.2",
"chart.js": "^4.4.3",
"cloudinary": "^2.4.0",
"cloudinary": "^2.2.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
@@ -38,24 +38,24 @@
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"firebase-admin": "^12.3.1",
"firebase-admin": "^12.2.0",
"graphql": "^16.9.0",
"graphql-request": "^6.1.0",
"graylog2": "^0.2.1",
"inline-css": "^4.0.2",
"intuit-oauth": "^4.1.2",
"json-2-csv": "^5.5.5",
"json-2-csv": "^5.5.4",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.5",
"node-persist": "^4.0.3",
"node-persist": "^4.0.1",
"nodemailer": "^6.9.14",
"phone": "^3.1.49",
"recursive-diff": "^1.0.9",
"rimraf": "^6.0.1",
"soap": "^1.1.1",
"soap": "^1.1.0",
"socket.io": "^4.7.5",
"ssh2-sftp-client": "^10.0.3",
"twilio": "^4.23.0",

View File

@@ -632,7 +632,7 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
}
}
//QB USA with GST and PST
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
bodyshop.accountingconfig &&
@@ -651,20 +651,9 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
Qty: 1
}
});
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero(jobs_by_pk.job_totals.totals.state_tax).toFormat(DineroQbFormat),
SalesItemLineDetail: {
...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}),
ItemRef: {
value: items[bodyshop.md_responsibility_centers.taxes.state.accountitem]
},
Qty: 1
}
});
}
} else {
//Handle insurance profile adjustments for Parts
//Handle insurance profile adjustments
Object.keys(job_totals.parts.adjustments).forEach((key) => {
if (qbo) {
//Going to always assume that we need to apply GST and PST for labor.
@@ -718,67 +707,6 @@ exports.default = function ({ bodyshop, jobs_by_pk, qbo = false, items, taxCodes
}
});
//Handle insurance profile adjustments for Labor and Materials
Object.keys(job_totals.rates).forEach((key) => {
if (
job_totals.rates[key] &&
job_totals.rates[key].adjustment &&
Dinero(job_totals.rates[key].adjustment).isZero() === false
) {
if (qbo) {
//Going to always assume that we need to apply GST and PST for labor.
const taxAccountCode = findTaxCode(
{
local: false,
federal: process.env.COUNTRY === "USA" ? false : true,
state: jobs_by_pk.state_tax_rate === 0 ? false : true
},
bodyshop.md_responsibility_centers.sales_tax_codes
);
const account = responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits[key.toUpperCase()]
);
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({
// jobline: jobline,
job: jobs_by_pk,
type: "storage"
})
: taxCodes[taxAccountCode];
InvoiceLineAdd.push({
DetailType: "SalesItemLineDetail",
Amount: Dinero(job_totals.rates[key].adjustment).toFormat(DineroQbFormat),
Description: `${account.accountdesc} - Adjustment`,
SalesItemLineDetail: {
...(jobs_by_pk.class ? { ClassRef: { value: classes[jobs_by_pk.class] } } : {}),
ItemRef: {
value: items[account.accountitem]
},
TaxCodeRef: {
value: QboTaxId
},
Qty: 1
}
});
} else {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits[key.toUpperCase()]
).accountitem
},
Desc: "Storage",
Quantity: 1,
Amount: Dinero(job_totals.rates[key].adjustment).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: bodyshop.md_responsibility_centers.taxes.itemexemptcode || "NON"
}
});
}
}
});
const QboTaxId =
process.env.COUNTRY === "USA"
? CheckQBOUSATaxID({

View File

@@ -273,7 +273,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
return result && result.json && result.json.Bill;
} catch (error) {
logger.log("qbo-payables-error", "DEBUG", req.user.email, bill.id, {
error: error, //(error && error.authResponse && error.authResponse.body) || (error && error.message),
error: (error && error.authResponse && error.authResponse.body) || (error && error.message),
method: "InsertBill"
});
throw error;

View File

@@ -360,10 +360,12 @@ function calculateAllocations(connectionData, job) {
}
}
if (InstanceManager({ rome: true })) {
//profile level adjustments for parts
//profile level adjustments
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
const accountName = selectedDmsAllocationConfig.profits[key];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
@@ -378,24 +380,6 @@ function calculateAllocations(connectionData, job) {
);
}
});
//profile level adjustments for labor and materials
Object.keys(job.job_totals.rates).forEach((key) => {
if (job.job_totals.rates[key] && job.job_totals.rates[key].adjustment && Dinero(job.job_totals.rates[key].adjustment).isZero() === false) {
const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates[key].adjustments));
} else {
CdkBase.createLogEvent(
connectionData,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
}
});
}
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {

View File

@@ -33,7 +33,7 @@ const ftpSetup = {
exports.default = async (req, res) => {
//Query for the List of Bodyshop Clients.
logger.log("kaizen-start", "DEBUG", "api", null, null);
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"];
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE"];
const { bodyshops } = await client.request(queries.GET_KAIZEN_SHOPS, {
imexshopid: kaizenShopsIDs

View File

@@ -1527,8 +1527,6 @@ exports.QUERY_JOB_COSTING_DETAILS = ` query QUERY_JOB_COSTING_DETAILS($id: uuid!
ca_bc_pvrt
ca_customer_gst
dms_allocation
cieca_pfl
materials
joblines(where: { removed: { _eq: false } }) {
id
db_ref
@@ -1643,8 +1641,6 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
ca_bc_pvrt
ca_customer_gst
dms_allocation
cieca_pfl
materials
joblines(where: {removed: {_eq: false}}) {
id
db_ref
@@ -1870,8 +1866,6 @@ exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
scheduled_in
actual_in
ca_bc_pvrt
cieca_pfl
materials
timetickets {
id
actualhrs

View File

@@ -286,45 +286,9 @@ function GenerateCostingData(job) {
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
let laborAmount = Dinero();
laborAmount = Dinero({
const laborAmount = Dinero({
amount: Math.round((job[rateName] || 0) * 100)
}).multiply(val.mod_lb_hrs || 0);
if (
job.cieca_pfl &&
job.cieca_pfl[val.mod_lbr_ty.toUpperCase()] &&
job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp !== 0
) {
let adjp = 0;
if (
val.mod_lbr_ty === "la1" ||
val.mod_lbr_ty === "la2" ||
val.mod_lbr_ty === "la3" ||
val.mod_lbr_ty === "la4"
) {
adjp =
Math.abs(job.cieca_pfl["LAU"].lbr_adjp) > 1
? job.cieca_pfl["LAU"].lbr_adjp
: job.cieca_pfl["LAU"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
} else {
if (job.cieca_pfl[val.mod_lbr_ty.toUpperCase()]) {
adjp =
Math.abs(job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp) > 1
? job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp
: job.cieca_pfl[val.mod_lbr_ty.toUpperCase()].lbr_adjp * 100; //Adjust lbr_adjp to whole number
} else {
adjp =
Math.abs(job.cieca_pfl["LAB"].lbr_adjp) > 1
? job.cieca_pfl["LAB"].lbr_adjp
: job.cieca_pfl["LAB"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
}
}
laborAmount = laborAmount.add(
laborAmount.percentage(adjp < 0 ? adjp * -1 : adjp).multiply(adjp < 0 ? -1 : 1)
);
}
if (!acc.labor[laborProfitCenter]) acc.labor[laborProfitCenter] = Dinero();
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(laborAmount);
@@ -353,7 +317,7 @@ function GenerateCostingData(job) {
if (!partsProfitCenter)
console.log("Unknown cost/profit center mapping for parts.", val.line_desc, val.part_type);
let partsAmount = Dinero({
const partsAmount = Dinero({
amount: val.act_price_before_ppc
? Math.round(val.act_price_before_ppc * 100)
: Math.round(val.act_price * 100)
@@ -374,33 +338,6 @@ function GenerateCostingData(job) {
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
: Dinero()
);
// Profile Discount for Parts
if (job.parts_tax_rates && job.parts_tax_rates[val.part_type.toUpperCase()]) {
if (
job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp !== undefined &&
job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp >= 0
) {
const discountRate =
Math.abs(job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp) > 1
? parts_tajob.parts_tax_rates_rates[val.part_type.toUpperCase()].prt_discp
: job.parts_tax_rates[val.part_type.toUpperCase()].prt_discp * 100;
const disc = partsAmount.percentage(discountRate).multiply(-1);
partsAmount = partsAmount.add(disc);
}
if (
job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp !== undefined &&
job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp >= 0
) {
const markupRate =
Math.abs(job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp) > 1
? job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp
: job.parts_tax_rates[val.part_type.toUpperCase()].prt_mkupp * 100;
const markup = partsAmount.percentage(markupRate);
partsAmount = partsAmount.add(markup);
}
}
if (!acc.parts[partsProfitCenter]) acc.parts[partsProfitCenter] = Dinero();
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(partsAmount);
}
@@ -476,7 +413,6 @@ function GenerateCostingData(job) {
if (!hasMapaLine) {
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MAPA"]
].add(
@@ -484,26 +420,10 @@ function GenerateCostingData(job) {
amount: Math.round((job.rate_mapa || 0) * 100)
}).multiply(materialsHours.mapaHrs || 0)
);
let adjp = 0;
if (job.materials["MAPA"] && job.materials["MAPA"].mat_adjp) {
adjp =
Math.abs(job.materials["MAPA"].mat_adjp) > 1
? job.materials["MAPA"].mat_adjp
: job.materials["MAPA"].mat_adjp * 100; //Adjust mat_adjp to whole number
}
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MAPA"]
].add(
jobLineTotalsByProfitCenter.additional[defaultProfits["MAPA"]]
.percentage(adjp < 0 ? adjp * -1 : adjp)
.multiply(adjp < 0 ? -1 : 1)
);
}
if (!hasMashLine) {
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]])
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = Dinero();
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MASH"]
].add(
@@ -511,21 +431,6 @@ function GenerateCostingData(job) {
amount: Math.round((job.rate_mash || 0) * 100)
}).multiply(materialsHours.mashHrs || 0)
);
let adjp = 0;
if (job.materials["MASH"] && job.materials["MASH"].mat_adjp) {
adjp =
Math.abs(job.materials["MASH"].mat_adjp) > 1
? job.materials["MASH"].mat_adjp
: job.materials["MASH"].mat_adjp * 100; //Adjust mat_adjp to whole number
}
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]] = jobLineTotalsByProfitCenter.additional[
defaultProfits["MASH"]
].add(
jobLineTotalsByProfitCenter.additional[defaultProfits["MASH"]]
.percentage(adjp < 0 ? adjp * -1 : adjp)
.multiply(adjp < 0 ? -1 : 1)
);
}
//Is it a DMS Setup?

View File

@@ -331,8 +331,6 @@ async function CalculateRatesTotals({ job, client }) {
//Skip calculating mapa and mash if we got the amounts.
if (!((property === "mapa" && hasMapaLine) || (property === "mash" && hasMashLine))) {
if (!ret[property].total) {
ret[property].base = Dinero();
ret[property].adjustment = Dinero();
ret[property].total = Dinero();
}
let threshold;
@@ -351,50 +349,13 @@ async function CalculateRatesTotals({ job, client }) {
}
}
const base = Dinero({
const total = Dinero({
amount: Math.round((ret[property].rate || 0) * 100)
}).multiply(ret[property].hours);
let adjp = 0;
if (property === "mapa" || property === "mash") {
if (job.materials[property.toUpperCase()] && job.materials[property.toUpperCase()].mat_adjp) {
adjp =
Math.abs(job.materials[property.toUpperCase()].mat_adjp) > 1
? job.materials[property.toUpperCase()].mat_adjp
: job.materials[property.toUpperCase()].mat_adjp * 100; //Adjust mat_adjp to whole number
}
} else {
if (property === "la1" || property === "la2" || property === "la3" || property === "la4") {
if (job.cieca_pfl["LAU"] && job.cieca_pfl["LAU"].lbr_adjp) {
adjp =
Math.abs(job.cieca_pfl["LAU"].lbr_adjp) > 1
? job.cieca_pfl["LAU"].lbr_adjp
: job.cieca_pfl["LAU"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
}
} else {
if (job.cieca_pfl[property.toUpperCase()] && job.cieca_pfl[property.toUpperCase()].lbr_adjp) {
adjp =
Math.abs(job.cieca_pfl[property.toUpperCase()].lbr_adjp) > 1
? job.cieca_pfl[property.toUpperCase()].lbr_adjp
: job.cieca_pfl[property.toUpperCase()].lbr_adjp * 100; //Adjust lbr_adjp to whole number
} else {
if (job.cieca_pfl["LAB"].lbr_adjp) {
adjp =
Math.abs(job.cieca_pfl["LAB"].lbr_adjp) > 1
? job.cieca_pfl["LAB"].lbr_adjp
: job.cieca_pfl["LAB"].lbr_adjp * 100; //Adjust lbr_adjp to whole number
}
}
}
}
const adjustment = base.percentage(adjp < 0 ? adjp * -1 : adjp).multiply(adjp < 0 ? -1 : 1);
const total = base.add(adjustment);
if (threshold && total.greaterThanOrEqual(threshold)) {
ret[property].total = ret[property].total.add(threshold);
} else {
ret[property].base = ret[property].base.add(base);
ret[property].adjustment = ret[property].adjustment.add(adjustment);
ret[property].total = ret[property].total.add(total);
}
}
@@ -742,19 +703,18 @@ function CalculateTaxesTotals(job, otherTotals) {
//Potential issue here with Sublet Calculation. Sublets are calculated under labor in Mitchell, but it's done in IO
//Under the parts rates.
let stateTax = Dinero();
// let additionalItemsTax = Dinero(); //This is not used.
let statePartsTax = Dinero();
let additionalItemsTax = Dinero();
let us_sales_tax_breakdown;
// This is not referenced in the code base.
//Audatex sends additional glass part types. IO-774
// const BackupGlassTax =
// job.parts_tax_rates &&
// (job.parts_tax_rates.PAGD ||
// job.parts_tax_rates.PAGF ||
// job.parts_tax_rates.PAGP ||
// job.parts_tax_rates.PAGQ ||
// job.parts_tax_rates.PAGR);
const BackupGlassTax =
job.parts_tax_rates &&
(job.parts_tax_rates.PAGD ||
job.parts_tax_rates.PAGF ||
job.parts_tax_rates.PAGP ||
job.parts_tax_rates.PAGQ ||
job.parts_tax_rates.PAGR);
const taxableAmounts = {
PAA: Dinero(),
@@ -918,27 +878,11 @@ function CalculateTaxesTotals(job, otherTotals) {
} else if (key.startsWith("LA")) {
//Labor.
for (let tyCounter = 1; tyCounter <= 5; tyCounter++) {
if (key === "LA1" || key === "LA2" || key === "LA3" || key === "LA4") {
if (IsTrueOrYes(pfl["LAU"][`lbr_tx_in${tyCounter}`])) {
//This amount is taxable for this type.
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
taxableAmounts[key]
);
}
} else if (key === "LAA" && !pfl[key]) {
if (IsTrueOrYes(pfl["LAB"][`lbr_tx_in${tyCounter}`])) {
//This amount is taxable for this type.
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
taxableAmounts[key]
);
}
} else {
if (IsTrueOrYes(pfl[key][`lbr_tx_in${tyCounter}`])) {
//This amount is taxable for this type.
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
taxableAmounts[key]
);
}
if (IsTrueOrYes(pfl[key][`lbr_tx_in${tyCounter}`])) {
//This amount is taxable for this type.
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
taxableAmounts[key]
);
}
}
} else if (key === "TOW") {
@@ -975,6 +919,7 @@ function CalculateTaxesTotals(job, otherTotals) {
Object.keys(taxableAmountsByTier).forEach((taxTierKey) => {
taxable_adjustment = taxableAmountsByTier[taxTierKey].multiply(percent_of_adjustment);
console.log("🚀 ~ taxableAmountsByTier ~ taxable_adjustment:", taxable_adjustment);
if (job.adjustment_bottom_line > 0) {
taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].add(taxable_adjustment);
} else {
@@ -992,8 +937,8 @@ function CalculateTaxesTotals(job, otherTotals) {
let tyCounter = taxTierKey[2]; //Get the number from the key.
//i represents the tax number. If we got here, this type of tax is applicable. Now we need to add based on the thresholds.
for (let threshCounter = 1; threshCounter <= 5; threshCounter++) {
const thresholdAmount = parseFloat(job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]) || 0;
const thresholdTaxRate = parseFloat(job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]) || 0;
const thresholdAmount = parseFloat(job.cieca_pft[`ty${tyCounter}_thres${threshCounter}`]);
const thresholdTaxRate = parseFloat(job.cieca_pft[`ty${tyCounter}_rate${threshCounter}`]);
let taxableAmountInThisThreshold;
if (
@@ -1001,7 +946,7 @@ function CalculateTaxesTotals(job, otherTotals) {
InstanceMgr({
imex: false,
rome: thresholdAmount === 0 && parseInt(tyCounter) === 1,
promanager: "USE_ROME"
promanager: thresholdAmount === 0 && parseInt(tyCounter) === 1
})
) {
//
@@ -1035,10 +980,10 @@ function CalculateTaxesTotals(job, otherTotals) {
}
});
console.log("*** Total Tax by Tier Amounts***");
console.table(JSON.parse(JSON.stringify(totalTaxByTier)));
// console.log("*** Total Tax by Tier Amounts***");
// console.table(JSON.parse(JSON.stringify(totalTaxByTier)));
stateTax = stateTax
statePartsTax = statePartsTax
.add(totalTaxByTier.ty1Tax)
.add(totalTaxByTier.ty2Tax)
.add(totalTaxByTier.ty3Tax)
@@ -1046,18 +991,17 @@ function CalculateTaxesTotals(job, otherTotals) {
.add(totalTaxByTier.ty5Tax)
.add(totalTaxByTier.ty6Tax);
us_sales_tax_breakdown = totalTaxByTier;
//console.log("Tiered Taxes Total for Parts/Labor", stateTax.toFormat());
//console.log("Tiered Taxes Total for Parts/Labor", statePartsTax.toFormat());
// This is not in use as such commented out.
// let laborTaxTotal = Dinero();
let laborTaxTotal = Dinero();
// if (Object.keys(job.cieca_pfl).length > 0) {
// //Ignore it now, we have calculated it above.
// //This was previously used for JCS before parts were also calculated at a different rate.
// } else {
// //We don't have it, just add in how it was before.
// laborTaxTotal = otherTotals.rates.subtotal.percentage((job.tax_lbr_rt || 0) * 100); // THis is currently using the lbr tax rate from PFH not PFL.
// }
if (Object.keys(job.cieca_pfl).length > 0) {
//Ignore it now, we have calculated it above.
//This was previously used for JCS before parts were also calculated at a different rate.
} else {
//We don't have it, just add in how it was before.
laborTaxTotal = otherTotals.rates.subtotal.percentage((job.tax_lbr_rt || 0) * 100); // THis is currently using the lbr tax rate from PFH not PFL.
}
//console.log("Labor Tax Total", laborTaxTotal.toFormat());
@@ -1066,9 +1010,9 @@ function CalculateTaxesTotals(job, otherTotals) {
federal_tax: subtotal
.percentage((job.federal_tax_rate || 0) * 100)
.add(otherTotals.additional.pvrt.percentage((job.federal_tax_rate || 0) * 100)),
stateTax,
statePartsTax,
us_sales_tax_breakdown,
state_tax: stateTax,
state_tax: statePartsTax,
local_tax: subtotal.percentage((job.local_tax_rate || 0) * 100)
};
ret.total_repairs = ret.subtotal