Compare commits

..

31 Commits

Author SHA1 Message Date
Patrick Fic
865f4776d0 IO-233 Mixdata schema updates and API. 2022-04-28 09:54:15 -07:00
Patrick Fic
ad6d1202f2 IO-227 Begin PPG 2 way communication. 2022-04-27 16:03:53 -07:00
Patrick Fic
3db613da7f IO-1847 Allow $0 labor line with type selected. 2022-04-26 16:01:50 -07:00
Patrick Fic
c48b0d7b99 IO-1847 Add blank line if no sales lines for QBD export. 2022-04-26 15:56:03 -07:00
Patrick Fic
15cdcdfbea IO-1846 add prior succesful export indicator. 2022-04-26 15:25:51 -07:00
Patrick Fic
39b7280595 Allow clear of deductible status. 2022-04-26 13:38:16 -07:00
Patrick Fic
273542f93b IO-1532 Add job transition tracking server method. 2022-04-26 13:38:07 -07:00
Patrick Fic
6d01199185 IO-1831 Skip PBS vehicle search with no vin 2022-04-25 14:54:36 -07:00
Patrick Fic
db0ade9791 IO-1680 Order as In House from Job View 2022-04-25 14:40:23 -07:00
Patrick Fic
cbad157ded Merged in release/2022-04-22 (pull request #454)
Release/2022 04 22
2022-04-24 20:29:31 +00:00
Patrick Fic
a1f6f2fe4c IO-1477 Update to joblines_status view. 2022-04-22 15:14:02 -07:00
Patrick Fic
f72169d98f Header-Footer template render updates. 2022-04-22 15:12:20 -07:00
Patrick Fic
478e03cbe7 Resolve PO and Bills search on PLI screen. 2022-04-22 11:00:35 -07:00
Patrick Fic
c7389cc093 Updated footer minimum. 2022-04-22 10:37:33 -07:00
Patrick Fic
e659204e8f CNR Modifications. 2022-04-22 10:36:00 -07:00
Patrick Fic
07e6344812 IO-1839 Add filing coversheet landscape. 2022-04-21 12:36:04 -07:00
Patrick Fic
73e50df21b Added missing translations. 2022-04-21 12:13:07 -07:00
Patrick Fic
80f92203ca IO-1834 Add more job info and filtering to scoreboard jobs display. 2022-04-21 12:10:39 -07:00
Patrick Fic
eab9aca3d4 IO-1840 Add to scoreboard from production detail view. 2022-04-21 11:51:47 -07:00
Patrick Fic
49818cc043 Modification to Credits not Received. 2022-04-21 11:45:41 -07:00
Patrick Fic
51843f364b Add footer path. 2022-04-21 10:04:39 -07:00
Patrick Fic
7d8bbe69bd IO-1366 Add additional audit trail items. 2022-04-18 16:18:57 -07:00
Patrick Fic
6998a11a3a Add refresh button to job detail page. 2022-04-18 15:34:12 -07:00
Patrick Fic
65bf81b349 IO-1829 Add additional columns to visual board. 2022-04-18 14:48:56 -07:00
Patrick Fic
65783cde07 Update parts status counter to exclude sublets. 2022-04-18 14:34:36 -07:00
Patrick Fic
74297052d4 IO-1830 Add Parts Status to Production Boards. 2022-04-18 14:28:53 -07:00
Patrick Fic
988c3a9f22 IO-1477 Updated parts queue page. 2022-04-18 13:53:21 -07:00
Patrick Fic
c08713bfbe IO-1650 Added ready jobs screen. 2022-04-18 13:07:52 -07:00
Patrick Fic
a10b5a2ee0 IO-1816 Refactor main page. 2022-04-18 11:37:56 -07:00
Patrick Fic
920ebdde42 Merged in release/2022-04-15 (pull request #448)
Release/2022 04 15
2022-04-14 17:48:45 +00:00
Patrick Fic
e31a7eb65f Resolve third party payer template issue. 2022-04-14 10:23:09 -07:00
86 changed files with 10614 additions and 3240 deletions

21
.eslintrc.json Normal file
View File

@@ -0,0 +1,21 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"no-console": "off"
},
"settings": {},
"plugins": ["cypress"]
}

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2">
<babeledit_project version="1.2" be_version="2.7.1">
<!--
BabelEdit project file
@@ -1243,6 +1243,69 @@
<folder_node>
<name>messages</name>
<children>
<concept_node>
<name>admin_jobmarkexported</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>admin_jobmarkforreexport</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>admin_jobunvoid</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>billposted</name>
<definition_loaded>false</definition_loaded>
@@ -2611,6 +2674,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>calculatedcreditsnotreceived</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>creditsnotreceived</name>
<definition_loaded>false</definition_loaded>
@@ -5736,6 +5820,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>list-ready</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>partsqueue</name>
<definition_loaded>false</definition_loaded>
@@ -7629,6 +7734,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>additional_board_statuses</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>color</name>
<definition_loaded>false</definition_loaded>
@@ -8028,6 +8154,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ready_statuses</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<concept_node>
@@ -14346,6 +14493,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>priorsuccesfulexport</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
@@ -15862,6 +16030,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>refresh</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>required</name>
<definition_loaded>false</definition_loaded>
@@ -23147,6 +23336,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>partsstatus</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>pas</name>
<definition_loaded>false</definition_loaded>
@@ -23299,6 +23509,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>queued_for_parts</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>rate_ats</name>
<definition_loaded>false</definition_loaded>
@@ -26907,6 +27138,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>calculatedcreditsnotreceived</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>creditmemos</name>
<definition_loaded>false</definition_loaded>
@@ -29640,6 +29892,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>readyjobs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>recent</name>
<definition_loaded>false</definition_loaded>
@@ -32143,6 +32416,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>orderinhouse</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
</children>
@@ -32305,6 +32599,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>saving</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>updating</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -32373,6 +32709,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>cm_received</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>comments</name>
<definition_loaded>false</definition_loaded>
@@ -32798,6 +33155,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>mark_as_received</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>newpartsorder</name>
<definition_loaded>false</definition_loaded>
@@ -32992,6 +33370,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>line_updated</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>received</name>
<definition_loaded>false</definition_loaded>
@@ -34867,6 +35266,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>filing_coversheet_landscape</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>filing_coversheet_portrait</name>
<definition_loaded>false</definition_loaded>
@@ -36665,6 +37085,27 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>actual_in</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>alert</name>
<definition_loaded>false</definition_loaded>
@@ -37116,6 +37557,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>partsstatus</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>production_note</name>
<definition_loaded>false</definition_loaded>
@@ -41811,6 +42273,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobs-ready</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>owner-detail</name>
<definition_loaded>false</definition_loaded>
@@ -42821,6 +43304,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>readyjobs</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>resetpassword</name>
<definition_loaded>false</definition_loaded>

View File

@@ -3,40 +3,35 @@
"version": "0.2.1",
"private": true,
"proxy": "http://localhost:4000",
"browser": {
"fs": false,
"path": false,
"os": false
},
"dependencies": {
"@apollo/client": "^3.5.10",
"@asseinfo/react-kanban": "^2.2.0",
"@craco/craco": "^6.4.3",
"@fingerprintjs/fingerprintjs": "^3.3.3",
"@jsreport/browser-client": "^3.1.0",
"@sentry/react": "^6.19.6",
"@sentry/tracing": "^6.19.6",
"@splitsoftware/splitio-react": "^1.4.0",
"@stripe/react-stripe-js": "^1.7.1",
"@stripe/stripe-js": "^1.27.0",
"@tanem/react-nprogress": "^5.0.0",
"@tanem/react-nprogress": "^4.0.12",
"antd": "^4.19.5",
"apollo-link-logger": "^2.0.0",
"axios": "^0.26.1",
"craco-less": "^2.0.0",
"craco-less": "^1.20.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.0.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.6.10",
"firebase": "^9.6.11",
"graphql": "^16.3.0",
"i18next": "^21.6.16",
"i18next-browser-languagedetector": "^6.1.4",
"jsoneditor": "^9.7.4",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.51",
"logrocket": "^2.2.1",
"markerjs2": "^2.21.0",
"markerjs2": "^2.21.1",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"phone": "^3.1.15",
@@ -45,11 +40,11 @@
"query-string": "^7.1.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^18.0.0",
"react-big-calendar": "^0.40.0",
"react": "^17.0.2",
"react-big-calendar": "^0.38.2",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^18.0.0",
"react-dom": "^17.0.2",
"react-drag-listview": "^0.1.9",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.3.4",
@@ -74,18 +69,18 @@
"styled-components": "^5.3.5",
"subscriptions-transport-ws": "^0.11.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.5.3",
"workbox-broadcast-update": "^6.5.3",
"workbox-cacheable-response": "^6.5.3",
"workbox-core": "^6.5.3",
"workbox-expiration": "^6.5.3",
"workbox-google-analytics": "^6.5.3",
"workbox-navigation-preload": "^6.5.3",
"workbox-precaching": "^6.5.3",
"workbox-range-requests": "^6.5.3",
"workbox-routing": "^6.5.3",
"workbox-strategies": "^6.5.3",
"workbox-streams": "^6.5.3",
"workbox-background-sync": "^6.5.2",
"workbox-broadcast-update": "^6.5.2",
"workbox-cacheable-response": "^6.5.2",
"workbox-core": "^6.5.2",
"workbox-expiration": "^6.5.2",
"workbox-google-analytics": "^6.5.2",
"workbox-navigation-preload": "^6.5.2",
"workbox-precaching": "^6.5.2",
"workbox-range-requests": "^6.5.2",
"workbox-routing": "^6.5.2",
"workbox-strategies": "^6.5.2",
"workbox-streams": "^6.5.2",
"yauzl": "^2.10.0"
},
"scripts": {
@@ -124,9 +119,9 @@
"devDependencies": {
"@sentry/webpack-plugin": "^1.18.8",
"@testing-library/cypress": "^8.0.2",
"cypress": "^9.5.4",
"cypress": "^9.5.3",
"eslint-plugin-cypress": "^2.12.1",
"react-error-overlay": "6.0.11",
"react-error-overlay": "6.0.10",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2"
}

BIN
client/src/assets/banner4.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

View File

@@ -13,6 +13,7 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -131,11 +132,9 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
},
{
title: t("general.labels.actions"),

View File

@@ -13,6 +13,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -130,11 +131,9 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
},
{
title: t("general.labels.actions"),

View File

@@ -14,6 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -139,12 +140,9 @@ export function AccountingReceivablesTableComponent({
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
},
{
title: t("general.labels.actions"),

View File

@@ -0,0 +1,136 @@
import { Checkbox, Form, Skeleton, Typography } from "antd";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
import "./bill-cm-returns-table.styles.scss";
export default function BillCmdReturnsTableComponent({
form,
loadOutstandingReturns,
returnLoading,
returnData,
}) {
const { t } = useTranslation();
useEffect(() => {
if (returnData) {
form.setFieldsValue({
outstanding_returns: returnData.parts_order_lines,
});
}
}, [returnData, form]);
return (
<Form.Item
shouldUpdate={(prev, cur) =>
prev.jobid !== cur.jobid ||
prev.is_credit_memo !== cur.is_credit_memo ||
prev.vendorid !== cur.vendorid
}
noStyle
>
{() => {
const isReturn = form.getFieldValue("is_credit_memo");
if (!isReturn) {
return null;
}
if (returnLoading) return <Skeleton />;
return (
<Form.List name="outstanding_returns">
{(fields, { add, remove, move }) => {
return (
<>
<Typography.Title level={4}>
{t("bills.labels.creditsnotreceived")}
</Typography.Title>
<table className="bill-cm-returns-table">
<thead>
<tr>
<th>{t("parts_orders.fields.line_desc")}</th>
<th>{t("parts_orders.fields.part_type")}</th>
<th>{t("parts_orders.fields.quantity")}</th>
<th>{t("parts_orders.fields.act_price")}</th>
<th>{t("parts_orders.fields.cost")}</th>
<th>{t("parts_orders.labels.mark_as_received")}</th>
</tr>
</thead>
<tbody>
{fields.map((field, index) => (
<tr key={field.key}>
<td>
<Form.Item
// label={t("joblines.fields.line_desc")}
key={`${index}line_desc`}
name={[field.name, "line_desc"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}part_type`}
name={[field.name, "part_type"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}quantity`}
name={[field.name, "quantity"]}
>
<ReadOnlyFormItemComponent />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}act_price`}
name={[field.name, "act_price"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cost`}
name={[field.name, "cost"]}
>
<ReadOnlyFormItemComponent type="currency" />
</Form.Item>
</td>
<td>
<Form.Item
span={2}
//label={t("joblines.fields.mod_lb_hrs")}
key={`${index}cm_received`}
name={[field.name, "cm_received"]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}}
</Form.List>
);
}}
</Form.Item>
);
}

View File

@@ -0,0 +1,19 @@
.bill-cm-returns-table {
table-layout: fixed;
width: 100%;
th,
td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
.ant-form-item {
margin-bottom: 0px !important;
}
}
tr:hover {
background-color: #f5f5f5;
}
}

View File

@@ -11,6 +11,7 @@ import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
@@ -47,6 +48,7 @@ function BillEnterModalContainer({
const [enterAgain, setEnterAgain] = useState(false);
const [insertBill] = useMutation(INSERT_NEW_BILL);
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
const [loading, setLoading] = useState(false);
const client = useApolloClient();
@@ -76,7 +78,8 @@ function BillEnterModalContainer({
}
setLoading(true);
const { upload, location, ...remainingValues } = values;
const { upload, location, outstanding_returns, ...remainingValues } =
values;
let adjustmentsToInsert = {};
@@ -156,6 +159,25 @@ function BillEnterModalContainer({
});
}
const markPolReceived =
outstanding_returns &&
outstanding_returns.filter((o) => o.cm_received === true);
if (markPolReceived && markPolReceived.length > 0) {
const r2 = await updatePartsOrderLines({
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
});
if (!!r2.errors) {
setLoading(false);
setEnterAgain(false);
notification["error"]({
message: t("parts_orders.errors.updating", {
message: JSON.stringify(r2.errors),
}),
});
}
}
if (!!r1.errors) {
setLoading(false);
setEnterAgain(false);

View File

@@ -47,6 +47,7 @@ export function BillFormComponent({
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -58,6 +59,14 @@ export function BillFormComponent({
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
useEffect(() => {
@@ -65,8 +74,8 @@ export function BillFormComponent({
}, [job, form]);
useEffect(() => {
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
const vendorId = form.getFieldValue("vendorid");
const vendorId = form.getFieldValue("vendorid");
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter(
(v) => v.id === vendorId
);
@@ -74,10 +83,25 @@ export function BillFormComponent({
setDiscount(matchingVendors[0].discount);
}
}
if (form.getFieldValue("jobid")) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue("is_credit_memo") && vendorId) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
}
}, [form, setDiscount, vendorAutoCompleteOptions, loadLines]);
}, [
form,
loadOutstandingReturns,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
]);
return (
<div>
@@ -107,6 +131,13 @@ export function BillFormComponent({
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null)
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
}}
/>
@@ -228,8 +259,22 @@ export function BillFormComponent({
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
value === true &&
getFieldValue("jobid") &&
getFieldValue("vendorid")
) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: form.getFieldValue("vendorid"),
},
});
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&

View File

@@ -6,6 +6,8 @@ import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import BillFormComponent from "./bill-form.component";
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -27,20 +29,34 @@ export function BillFormContainer({
GET_JOB_LINES_TO_ENTER_BILL
);
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] =
useLazyQuery(QUERY_UNRECEIVED_LINES);
return (
<BillFormComponent
disabled={disabled}
form={form}
billEdit={billEdit}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
/>
<>
<BillFormComponent
disabled={disabled}
form={form}
billEdit={billEdit}
vendorAutoCompleteOptions={
VendorAutoCompleteData && VendorAutoCompleteData.vendors
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
loadOutstandingReturns={loadOutstandingReturns}
/>
{!billEdit && (
<BillCmdReturnsTableComponent
form={form}
loadOutstandingReturns={loadOutstandingReturns}
returnLoading={returnLoading}
returnData={returnData}
/>
)}
</>
);
}
export default connect(mapStateToProps, null)(BillFormContainer);

View File

@@ -43,6 +43,8 @@ export function BillsListTableComponent({
});
// const search = queryString.parse(useLocation().search);
// const selectedBill = search.billid;
const [searchText, setSearchText] = useState("");
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const { refetch } = billsQuery;
@@ -59,7 +61,6 @@ export function BillsListTableComponent({
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
}
onClick={() => {
console.log(record);
setPartsOrderContext({
actions: {},
context: {
@@ -167,6 +168,24 @@ export function BillsListTableComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const filteredBills = bills
? searchText === ""
? bills
: bills.filter(
(b) =>
(b.invoice_number || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.total || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return (
<Card
title={t("bills.labels.bills")}
@@ -207,8 +226,10 @@ export function BillsListTableComponent({
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
@@ -221,7 +242,7 @@ export function BillsListTableComponent({
}}
columns={columns}
rowKey="id"
dataSource={bills}
dataSource={filteredBills}
onChange={handleTableChange}
/>
</Card>

View File

@@ -0,0 +1,25 @@
import React from "react";
import { WarningOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
const style = {
fontWeight: "bold",
color: "green",
};
export default function ExportLogsCountDisplay({ logs }) {
const success = logs.filter((e) => e.successful).length;
const attempts = logs.length;
const { t } = useTranslation();
return (
<Space style={success > 0 ? style : {}}>
{`${success}/${attempts}`}
{success > 0 && (
<Tooltip title={t("exportlogs.labels.priorsuccesfulexport")}>
<WarningOutlined />
</Tooltip>
)}
</Space>
);
}

View File

@@ -3,6 +3,7 @@ import Icon, {
BarChartOutlined,
CarFilled,
ClockCircleFilled,
CheckCircleOutlined,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
@@ -108,6 +109,9 @@ function Header({
<Menu.Item key="activejobs" icon={<FileFilled />}>
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
</Menu.Item>
<Menu.Item key="readyjobs" icon={<CheckCircleOutlined />}>
<Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
</Menu.Item>
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
</Menu.Item>

View File

@@ -49,11 +49,11 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
GenerateDocument(
{
name: TemplateList("job_special").thirdpartypayer.key,
name: TemplateList("job_special").special_thirdpartypayer.key,
variables: { id: jobId },
context: restVals,
},
{ subject: TemplateList("job_special").thirdpartypayer.subject },
{ subject: TemplateList("job_special").special_thirdpartypayer.subject },
sendtype
);
};

View File

@@ -26,6 +26,8 @@ export default function JobBillsTotalComponent({
let billCms = Dinero();
let lbrAdjustments = Dinero();
let totalReturns = Dinero();
let totalReturnsMarkedNotReceived = Dinero();
let totalReturnsMarkedReceived = Dinero();
partsOrders.forEach((p) =>
p.parts_order_lines.forEach((pol) => {
@@ -35,6 +37,24 @@ export default function JobBillsTotalComponent({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
if (pol.cm_received === null) {
return; // Skip this calculation for bills posted prior to the CNR change.
} else {
if (pol.cm_received === false) {
totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add(
Dinero({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
} else {
totalReturnsMarkedReceived = totalReturnsMarkedReceived.add(
Dinero({
amount: Math.round((pol.act_price || 0) * 100),
}).multiply(pol.quantity)
);
}
}
}
})
);
@@ -73,7 +93,7 @@ export default function JobBillsTotalComponent({
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
return (
<Row gutter={16}>
@@ -213,6 +233,32 @@ export default function JobBillsTotalComponent({
value={totalReturns.toFormat()}
/>
</Tooltip>
<Tooltip
title={
<div
dangerouslySetInnerHTML={{
__html: t(
"jobs.labels.plitooltips.calculatedcreditsnotreceived"
),
}}
/>
}
>
<Statistic
title={t("bills.labels.calculatedcreditsnotreceived")}
valueStyle={{
color:
calculatedCreditsNotReceived.getAmount() <= 0
? "green"
: "red",
}}
value={
calculatedCreditsNotReceived.getAmount() >= 0
? calculatedCreditsNotReceived.toFormat()
: Dinero().toFormat()
}
/>
</Tooltip>
<Tooltip
title={
<div
@@ -225,11 +271,14 @@ export default function JobBillsTotalComponent({
<Statistic
title={t("bills.labels.creditsnotreceived")}
valueStyle={{
color: creditsNotReceived.getAmount() <= 0 ? "green" : "red",
color:
totalReturnsMarkedNotReceived.getAmount() <= 0
? "green"
: "red",
}}
value={
creditsNotReceived.getAmount() >= 0
? creditsNotReceived.toFormat()
totalReturnsMarkedNotReceived.getAmount() >= 0
? totalReturnsMarkedNotReceived.toFormat()
: Dinero().toFormat()
}
/>

View File

@@ -6,6 +6,7 @@ import {
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import {
@@ -42,6 +43,7 @@ import _ from "lodash";
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
import JobLinesExpander from "./job-lines-expander.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -54,6 +56,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
});
export function JobLinesComponent({
@@ -68,6 +72,7 @@ export function JobLinesComponent({
job,
setJobLineEditContext,
form,
setBillEnterContext,
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -386,6 +391,62 @@ export function JobLinesComponent({
</Space>
</Tag>
)}
<Button
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
jobRO ||
technician
}
onClick={() => {
// setPartsOrderContext({
// actions: { refetch: refetch },
// context: {
// jobId: job.id,
// job: job,
// linesToOrder: selectedLines,
// },
// });
setBillEnterContext({
actions: { refetch: refetch },
context: {
disableInvNumber: true,
job: { id: job.id },
bill: {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
isinhouse: true,
date: new moment(),
total: 0,
billlines: selectedLines.map((p) => {
return {
joblineid: p.id,
actual_price: p.act_price,
actual_cost: 0, //p.act_price,
line_desc: p.line_desc,
line_remarks: p.line_remarks,
part_type: p.part_type,
quantity: p.quantity || 1,
applicable_taxes: {
local: false,
state: false,
federal: false,
},
};
}),
},
},
});
//Clear out the selected lines. IO-785
setSelectedLines([]);
}}
>
<HomeOutlined />
{t("parts.actions.orderinhouse")}
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button
disabled={
(job && !job.converted) ||

View File

@@ -141,7 +141,9 @@ export function JobLinesUpsertModalComponent({
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!!getFieldValue("mod_lbr_ty") === !!value) {
if (
!!getFieldValue("mod_lbr_ty") === (!!value || value === 0)
) {
return Promise.resolve();
}
return Promise.reject(

View File

@@ -0,0 +1,80 @@
import React, { useMemo } from "react";
import { Row, Col, Tag, Tooltip } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts }) {
const partsStatus = useMemo(() => {
if (!parts) return null;
return parts.reduce(
(acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
return acc;
},
{
total: 0,
null: 0,
[bodyshop.md_order_statuses.default_bo]: 0,
[bodyshop.md_order_statuses.default_ordered]: 0,
[bodyshop.md_order_statuses.default_received]: 0,
[bodyshop.md_order_statuses.default_returned]: 0,
}
);
}, [bodyshop, parts]);
if (!parts) return null;
return (
<Row>
<Col span={4}>
<Tooltip title="Total">
<Tag>{partsStatus.total}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title="No Status">
<Tag color="gold">{partsStatus["null"]}</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue">
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green">
{partsStatus[bodyshop.md_order_statuses.default_received]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange">
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</Col>
<Col span={4}>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red">
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</Tag>
</Tooltip>
</Col>
</Row>
);
}

View File

@@ -1,24 +1,23 @@
import { Button, notification } from "antd";
import React, { useState } from "react";
import { useMutation } from "@apollo/client";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { Checkbox, notification, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
export default function JobRemoveFromPartsQueue({ jobId, refetch }) {
export default function JobRemoveFromPartsQueue({ checked, jobId }) {
const [updateJob] = useMutation(UPDATE_JOB);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleClick = async (e) => {
const handleChange = async (e) => {
setLoading(true);
const result = await updateJob({
variables: { jobId: jobId, job: { queued_for_parts: false } },
variables: { jobId: jobId, job: { queued_for_parts: e.target.checked } },
});
if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
if (refetch) refetch();
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -30,8 +29,9 @@ export default function JobRemoveFromPartsQueue({ jobId, refetch }) {
};
return (
<Button onClick={handleClick} loading={loading}>
{t("general.actions.remove")}
</Button>
<Space>
<Checkbox checked={checked} onChange={handleChange} />
{loading && <Spin size="small" />}
</Space>
);
}

View File

@@ -6,17 +6,20 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
export function JobsAdminStatus({ bodyshop, job }) {
export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
const { t } = useTranslation();
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
@@ -26,6 +29,10 @@ export function JobsAdminStatus({ bodyshop, job }) {
})
.then((r) => {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status),
});
// refetch();
})
.catch((error) => {

View File

@@ -7,8 +7,27 @@ import DateTimePicker from "../form-date-time-picker/form-date-time-picker.compo
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import moment from "moment";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
export default function JobsAdminDatesChange({ job }) {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsAdminDatesChange);
export function JobsAdminDatesChange({ insertAuditTrail, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
@@ -20,6 +39,23 @@ export default function JobsAdminDatesChange({ job }) {
variables: { jobId: job.id, job: values },
});
const changedAuditFields = form.getFieldsValue(
true,
(meta) => meta && meta.touched
);
Object.keys(changedAuditFields).forEach((key) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobfieldchange(
key,
changedAuditFields[key] instanceof moment
? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
: changedAuditFields[key]
),
});
});
if (!!!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
} else {

View File

@@ -8,18 +8,21 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobAdminMarkReexport);
export function JobAdminMarkReexport({ bodyshop, job }) {
export function JobAdminMarkReexport({ insertAuditTrail, bodyshop, job }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [markJobForReexport] = useMutation(gql`
@@ -78,6 +81,10 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -96,6 +103,10 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -4,21 +4,29 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
export function JobsAdminUnvoid({ bodyshop, job, currentUser }) {
export function JobsAdminUnvoid({
insertAuditTrail,
bodyshop,
job,
currentUser,
}) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [updateJob] = useMutation(gql`
@@ -84,6 +92,11 @@ mutation UNVOID_JOB($jobId: uuid!) {
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_unvoicejob(),
});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -146,7 +146,11 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel forceRender key="claim" header={t("menus.jobsdetail.claimdetail")}>
<Collapse.Panel
forceRender
key="claim"
header={t("menus.jobsdetail.claimdetail")}
>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
@@ -193,7 +197,8 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel forceRender
<Collapse.Panel
forceRender
key="financial"
header={t("menus.jobsdetail.financials")}
>
@@ -204,7 +209,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select>
<Select allowClear>
<Select.Option value="W">
{t("jobs.labels.deductible.waived")}
</Select.Option>

View File

@@ -97,7 +97,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
</Form.Item>
<Form.Item
nostyle
noStyle
shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}
>
{() => {

View File

@@ -0,0 +1,355 @@
import {
ExclamationCircleFilled,
PauseCircleOutlined,
SyncOutlined,
} from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Grid, Input, Space, Table } from "antd";
import queryString from "query-string";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function JobsReadyList({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { selected } = searchParams;
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
const readyStatuses = useMemo(() => {
if (bodyshop.md_ro_statuses.ready_statuses)
return bodyshop.md_ro_statuses.ready_statuses;
return bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) =>
s !== bodyshop.md_ro_statuses.default_invoiced &&
s !== bodyshop.md_ro_statuses.default_exported
);
}, [bodyshop.md_ro_statuses]);
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
variables: {
statuses: readyStatuses,
},
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
});
const { t } = useTranslation();
const history = useHistory();
const [searchText, setSearchText] = useState("");
if (error) return <AlertComponent message={error.message} type="error" />;
const jobs = data
? searchText === ""
? data.jobs
: data.jobs.filter(
(j) =>
(j.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.comments || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
(j.plate_no || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(j.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const handleOnRowClick = (record) => {
if (record) {
if (record.id) {
history.push({
search: queryString.stringify({
...searchParams,
selected: record.id,
}),
});
}
}
};
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder:
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
<Link
to={"/manage/jobs/" + record.id}
onClick={(e) => e.stopPropagation()}
>
<Space>
{record.ro_number || t("general.labels.na")}
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}
{record.suspended && (
<PauseCircleOutlined style={{ color: "orangered" }} />
)}
</Space>
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
return record.owner ? (
<Link
to={"/manage/owners/" + record.owner.id}
onClick={(e) => e.stopPropagation()}
>
<OwnerNameDisplay ownerObject={record} />
</Link>
) : (
<span>
<OwnerNameDisplay ownerObject={record} />
</span>
);
},
},
{
title: t("jobs.fields.ownr_ph1"),
dataIndex: "ownr_ph1",
key: "ownr_ph1",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
),
},
{
title: t("jobs.fields.ownr_ph2"),
dataIndex: "ownr_ph2",
key: "ownr_ph2",
ellipsis: true,
responsive: ["md"],
render: (text, record) => (
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
),
},
{
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filters:
(jobs &&
jobs
.map((j) => j.status)
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.status),
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
onClick={(e) => e.stopPropagation()}
>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}
</Link>
) : (
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
record.v_model_desc || ""
}`}</span>
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder:
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
},
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
key: "clm_no",
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
sortOrder:
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
render: (text, record) =>
`${record.clm_no || ""}${
record.po_number ? ` (PO: ${record.po_number})` : ""
}`,
},
{
title: t("jobs.fields.ins_co_nm"),
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
responsive: ["md"],
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
responsive: ["md"],
ellipsis: true,
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder:
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
),
},
{
title: t("jobs.fields.comment"),
dataIndex: "comment",
key: "comment",
ellipsis: true,
responsive: ["md"],
},
// {
// title: t("jobs.fields.owner_owing"),
// dataIndex: "owner_owing",
// key: "owner_owing",
// responsive: ["md"],
// render: (text, record) => (
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
// ),
// },
];
const scrollMapper = {
xs: true,
sm: true,
md: true,
lg: "100%",
xl: "100%",
xxl: "100%",
};
return (
<Card
title={t("titles.bc.jobs-ready")}
extra={
<Space wrap>
<span>({readyStatuses && readyStatuses.join(", ")})</span>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
/>
</Space>
}
>
<Table
loading={loading}
pagination={{ defaultPageSize: 50 }}
columns={columns}
rowKey="id"
dataSource={jobs}
scroll={{
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selected],
type: "radio",
}}
onChange={handleTableChange}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
},
};
}}
/>
</Card>
);
}
export default connect(mapStateToProps, null)(JobsReadyList);

View File

@@ -0,0 +1,66 @@
import { useMutation } from "@apollo/client";
import { Checkbox, notification, Space, Spin } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { MUTATION_UPDATE_PO_CM_REECEIVED } from "../../graphql/parts-orders.queries";
export default function PartsOrderCmReceived({
checked,
orderLineId,
partsOrderId,
}) {
const [updateLine] = useMutation(MUTATION_UPDATE_PO_CM_REECEIVED);
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleChange = async (e) => {
setLoading(true);
const result = await updateLine({
variables: {
partsLineId: orderLineId,
partsOrder: { cm_received: e.target.checked },
},
update(cache) {
cache.modify({
id: cache.identify({
id: partsOrderId,
__typename: "parts_orders",
}),
fields: {
parts_order_lines(ex, { readField }) {
console.log(ex);
return ex.map((lineref) => {
if (orderLineId.id !== readField("id", lineref)) {
lineref.cm_received = e.target.checked;
}
return lineref;
});
},
},
});
},
});
if (!!!result.errors) {
notification["success"]({
message: t("parts_orders.successes.line_updated"),
});
} else {
notification["error"]({
message: t("parts_orders.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setLoading(false);
};
return (
<Space>
<Checkbox checked={checked} onChange={handleChange} />
{loading && <Spin size="small" />}
</Space>
);
}

View File

@@ -29,6 +29,7 @@ import { alphaSort } from "../../utils/sorters";
import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import 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";
@@ -77,6 +78,7 @@ export function PartsOrderListTableComponent({
});
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [searchText, setSearchText] = useState("");
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
@@ -346,6 +348,23 @@ export function PartsOrderListTableComponent({
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",
@@ -403,6 +422,21 @@ export function PartsOrderListTableComponent({
);
};
const filteredPartsOrders = parts_orders
? searchText === ""
? parts_orders
: parts_orders.filter(
(b) =>
(b.order_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(b.vendor.name || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
return (
<Card
title={t("parts_orders.labels.parts_orders")}
@@ -413,8 +447,10 @@ export function PartsOrderListTableComponent({
</Button>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
@@ -438,7 +474,7 @@ export function PartsOrderListTableComponent({
}}
columns={columns}
rowKey="id"
dataSource={parts_orders}
dataSource={filteredPartsOrders}
onChange={handleTableChange}
/>
</Card>

View File

@@ -305,6 +305,7 @@ export function PartsOrderModalContainer({
quantity: value.part_qty,
job_line_id: isReturn ? value.joblineid : value.id,
part_type: value.part_type,
...(isReturn && { cm_received: false }),
});
return acc;
}, [])

View File

@@ -1,6 +1,7 @@
import {
CalendarOutlined,
EyeFilled,
DownloadOutlined,
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Col, Row, Space } from "antd";
@@ -14,6 +15,7 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
import "./production-board-card.styles.scss";
import moment from "moment";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
export default function ProductionBoardCard(
technician,
@@ -157,6 +159,16 @@ export default function ProductionBoardCard(
</Row>
</Col>
)} */}
{cardSettings && cardSettings.actual_in && card.actual_in && (
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
<Space>
<DownloadOutlined />
<DateTimeFormatter format="MM/DD">
{card.actual_in}
</DateTimeFormatter>
</Space>
</Col>
)}
{cardSettings &&
cardSettings.scheduled_completion &&
card.scheduled_completion && (
@@ -188,6 +200,11 @@ export default function ProductionBoardCard(
)}
</Col>
)}
{cardSettings && cardSettings.partsstatus && (
<Col span={24}>
<JobPartsQueueCount parts={card.joblines_status} />
</Col>
)}
</Row>
</Card>
);

View File

@@ -97,6 +97,13 @@ export default function ProductionBoardKanbanCardSettings({
>
<Switch />
</Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.actual_in")}
name="actual_in"
>
<Switch />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
@@ -131,6 +138,13 @@ export default function ProductionBoardKanbanCardSettings({
>
<Switch />
</Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.partsstatus")}
name="partsstatus"
>
<Switch />
</Form.Item>
<Form.Item
valuePropName="checked"
label={t("production.labels.stickyheader")}

View File

@@ -51,7 +51,10 @@ export function ProductionBoardKanbanComponent({
const { t } = useTranslation();
useEffect(() => {
const boardData = createBoardData(
bodyshop.md_ro_statuses.production_statuses,
[
...bodyshop.md_ro_statuses.production_statuses,
...(bodyshop.md_ro_statuses.additional_board_statuses || []),
],
data,
filter
);
@@ -61,13 +64,7 @@ export function ProductionBoardKanbanComponent({
});
setBoardLanes(boardData);
setIsMoving(false);
}, [
data,
setBoardLanes,
setIsMoving,
bodyshop.md_ro_statuses.production_statuses,
filter,
]);
}, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
const client = useApolloClient();

View File

@@ -23,6 +23,7 @@ import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.co
import ProductionListColumnComment from "./production-list-columns.comment.component";
import ProductionListColumnPartsReceived from "./production-list-columns.partsreceived.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const r = ({ technician, state, activeStatuses, bodyshop }) => {
return [
@@ -490,6 +491,14 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
<ProductionListColumnPartsReceived record={record} />
),
},
{
title: i18n.t("jobs.fields.partsstatus"),
dataIndex: "partsstatus",
key: "partsstatus",
render: (text, record) => (
<JobPartsQueueCount parts={record.joblines_status} record={record} />
),
},
];
};
export default r;

View File

@@ -21,6 +21,7 @@ import OwnerNameDisplay from "../owner-name-display/owner-name-display.component
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -59,7 +60,7 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) {
<PageHeader
title={theJob.ro_number}
extra={
<Space>
<Space wrap>
<ProductionRemoveButton jobId={theJob.id} />{" "}
<Button
onClick={() => {
@@ -76,6 +77,7 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) {
<PrinterFilled />
{t("jobs.actions.printCenter")}
</Button>
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
</Space>
}
/>

View File

@@ -1,13 +1,42 @@
import React from "react";
import { Dropdown, Button, Table, Space } from "antd";
import React, { useState } from "react";
import { Dropdown, Button, Table, Space, Card, Input } from "antd";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
import { DateFormatter } from "../../utils/DateFormatter";
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
export default function ScoreboardJobsList({ scoreBoardlist }) {
const { t } = useTranslation();
const [searchText, setSearchText] = useState("");
const jobs = scoreBoardlist
? searchText === ""
? scoreBoardlist
: scoreBoardlist.filter(
(sb) =>
(sb.job.ro_number || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.ownr_co_nm || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.ownr_fn || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.ownr_ln || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.v_model_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase()) ||
(sb.job.v_make_desc || "")
.toLowerCase()
.includes(searchText.toLowerCase())
)
: [];
const columns = [
{
@@ -20,7 +49,25 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
</Link>
),
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
ellipsis: true,
render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />,
},
{
title: t("jobs.fields.vehicle"),
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => (
<span>{`${record.job.v_model_yr || ""} ${
record.job.v_make_desc || ""
} ${record.job.v_model_desc || ""}`}</span>
),
},
{
title: t("scoreboard.fields.date"),
dataIndex: "date",
@@ -51,17 +98,29 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
];
const overlay = (
<div style={{ width: "50vw", padding: "1rem" }}>
<Card
style={{ maxWidth: "90vw", padding: "1rem" }}
onClick={(e) => e.stopPropagation()}
extra={
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
setSearchText(e.target.value);
}}
value={searchText}
enterButton
onClick={(e) => e.stopPropagation()}
/>
}
>
<Table
pagination={false}
columns={columns}
rowKey="id"
dataSource={scoreBoardlist}
scroll={{ x: true, y: "15rem" }}
style={{ padding: "1rem" }}
dataSource={jobs}
onClick={(e) => e.stopPropagation()}
/>
</div>
</Card>
);
return (

View File

@@ -25,10 +25,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
const values = useMemo(() => {
const dateHash = _.groupBy(scoreBoardlist, "date");
console.log(
"🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 31 ~ values ~ dateHash",
dateHash
);
let ret = {
todayBody: 0,
@@ -71,10 +67,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
return ret;
}, [scoreBoardlist]);
console.log(
"🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 51 ~ values ~ values",
values
);
return (
<Card

View File

@@ -165,6 +165,18 @@ export default function ShopInfoRbacComponent({ form }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.list-ready")}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={["md_rbac", "jobs:list-ready"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.admin")}
rules={[

View File

@@ -134,6 +134,44 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) {
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "ready_statuses"]}
label={t("bodyshop.fields.statuses.ready_statuses")}
rules={[
{
//required: true,
//message: t("general.validation.required"),
type: "array",
},
]}
>
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={["md_ro_statuses", "additional_board_statuses"]}
label={t("bodyshop.fields.statuses.additional_board_statuses")}
rules={[
{
//required: true,
//message: t("general.validation.required"),
type: "array",
},
]}
>
<Select mode="multiple">
{options.map((item, idx) => (
<Select.Option key={idx} value={item}>
{item}
</Select.Option>
))}
</Select>
</Form.Item>
<LayoutFormRow noDivider>
<Form.Item
label={t("bodyshop.fields.statuses.default_scheduled")}

View File

@@ -86,6 +86,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
job_line_id
part_type
cost
cm_received
jobline {
id
part_type
@@ -124,7 +125,7 @@ export const QUERY_BILLS_BY_JOBID = gql`
applicable_taxes
deductedfromlbr
lbr_adjustment
jobline{
jobline {
oem_partno
part_type
}
@@ -164,7 +165,7 @@ export const QUERY_BILL_BY_PK = gql`
cost_center
quantity
joblineid
jobline{
jobline {
oem_partno
part_type
}

View File

@@ -50,25 +50,13 @@ export const QUERY_PARTS_QUEUE = gql`
$limit: Int
$order: [jobs_order_by!]
) {
jobs_aggregate(
where: {
_and: [
{ status: { _in: $statuses } }
{ queued_for_parts: { _eq: true } }
]
}
) {
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
aggregate {
count(distinct: true)
}
}
jobs(
where: {
_and: [
{ status: { _in: $statuses } }
{ queued_for_parts: { _eq: true } }
]
}
where: { _and: [{ status: { _in: $statuses } }] }
offset: $offset
limit: $limit
order_by: $order
@@ -99,6 +87,12 @@ export const QUERY_PARTS_QUEUE = gql`
updated_at
vehicleid
ownerid
queued_for_parts
joblines_status {
count
part_type
status
}
}
}
`;
@@ -851,6 +845,11 @@ export const QUERY_JOB_CARD_DETAILS = gql`
count
status
}
joblines {
id
mod_lbr_ty
mod_lb_hrs
}
owner {
id
allow_text_message
@@ -1050,6 +1049,7 @@ export const UPDATE_JOB = gql`
production_vars
lbr_adjustments
suspended
queued_for_parts
}
}
}

View File

@@ -72,7 +72,7 @@ export const QUERY_PARTS_ORDER_OEC = gql`
part_type
}
job {
bodyshop{
bodyshop {
shopname
bill_tax_rates
}
@@ -292,6 +292,22 @@ export const DELETE_PARTS_ORDER = gql`
}
`;
export const MUTATION_UPDATE_PO_CM_REECEIVED = gql`
mutation MUTATION_UPDATE_PO_CM_REECEIVED(
$partsLineId: uuid!
$partsOrder: parts_order_lines_set_input
) {
update_parts_order_lines(
where: { id: { _eq: $partsLineId } }
_set: $partsOrder
) {
returning {
id
cm_received
}
}
}
`;
export const MUTATION_UPDATE_BO_ETA = gql`
mutation MUTATION_UPDATE_BO_ETA(
$partsLineId: uuid!
@@ -339,3 +355,36 @@ export const MUTATION_BACKORDER_PART_LINE = gql`
}
}
`;
export const QUERY_UNRECEIVED_LINES = gql`
query QUERY_UNRECEIVED_LINES($jobId: uuid!, $vendorId: uuid!) {
parts_order_lines(
where: {
parts_order: { jobid: { _eq: $jobId }, vendorid: { _eq: $vendorId } }
cm_received: { _neq: true }
}
) {
cm_received
id
line_desc
quantity
act_price
cost
oem_partno
}
}
`;
export const MUTATION_MARK_RETURN_RECEIVED = gql`
mutation MUTATION_MARK_RETURN_RECEIVED($partsLineIds: [uuid!]!) {
update_parts_order_lines(
where: { id: { _in: $partsLineIds } }
_set: { cm_received: true }
) {
returning {
id
cm_received
}
}
}
`;

View File

@@ -13,6 +13,12 @@ export const SUBSCRIPTION_SCOREBOARD = gql`
job {
id
ro_number
ownr_fn
ownr_ln
ownr_co_nm
v_make_desc
v_model_desc
v_model_yr
}
}
}

View File

@@ -3,7 +3,7 @@ import * as Sentry from "@sentry/react";
import "antd/dist/antd.less";
import Dinero from "dinero.js";
import React from "react";
import { createRoot } from "react-dom/client";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { PersistGate } from "redux-persist/integration/react";
@@ -39,9 +39,7 @@ if (process.env.NODE_ENV !== "development") {
});
}
const container = document.getElementById("root");
const root = createRoot(container); // createRoot(container!) if you use TypeScript
root.render(
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<PersistGate
@@ -51,7 +49,48 @@ root.render(
<AppContainer />
</PersistGate>
</BrowserRouter>
</Provider>
</Provider>,
document.getElementById("root")
);
// const onServiceWorkerUpdate = (registration) => {
// console.log("onServiceWorkerUpdate", registration);
// const btn = (
// <Space flex>
// <Button
// onClick={async () => {
// window.open("https://imex-online.noticeable.news/", "_blank");
// }}
// >
// {i18n.t("general.actions.viewreleasenotes")}
// </Button>
// <Button
// type="primary"
// onClick={async () => {
// if (registration && registration.waiting) {
// await registration.unregister();
// // Makes Workbox call skipWaiting()
// registration.waiting.postMessage({ type: "SKIP_WAITING" });
// // Once the service worker is unregistered, we can reload the page to let
// // the browser download a fresh copy of our app (invalidating the cache)
// window.location.reload();
// }
// }}
// >
// {i18n.t("general.actions.refresh")}
// </Button>
// </Space>
// );
// notification.open({
// icon: <AlertOutlined />,
// message: i18n.t("general.messages.newversiontitle"),
// description: i18n.t("general.messages.newversionmessage"),
// duration: 0,
// btn,
// key: "updateavailable",
// });
// };
// serviceWorkerRegistration.register({ onUpdate: onServiceWorkerUpdate });
reportWebVitals();

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Button } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import QueueAnim from 'rc-queue-anim';
import TweenOne from 'rc-tween-one';
import { isImg } from './utils';
import React from "react";
import { Button, Space } from "antd";
import { DownOutlined } from "@ant-design/icons";
import QueueAnim from "rc-queue-anim";
import TweenOne from "rc-tween-one";
import { isImg } from "./utils";
class Banner extends React.PureComponent {
render() {
@@ -15,12 +15,12 @@ class Banner extends React.PureComponent {
<div {...currentProps} {...dataSource.wrapper}>
<QueueAnim
key="QueueAnim"
type={['bottom', 'top']}
type={["bottom", "top"]}
delay={200}
{...dataSource.textWrapper}
>
<div key="title" {...dataSource.title}>
{typeof dataSource.title.children === 'string' &&
{typeof dataSource.title.children === "string" &&
dataSource.title.children.match(isImg) ? (
<img src={dataSource.title.children} width="100%" alt="img" />
) : (
@@ -30,13 +30,18 @@ class Banner extends React.PureComponent {
<div key="content" {...dataSource.content}>
{dataSource.content.children}
</div>
<Button ghost key="button" {...dataSource.button}>
{dataSource.button.children}
</Button>
<Space wrap>
<Button ghost key="button" {...dataSource.button}>
{dataSource.button.children}
</Button>
<Button type="primary" key="button2" {...dataSource.button2}>
{dataSource.button2.children}
</Button>
</Space>
</QueueAnim>
<TweenOne
animation={{
y: '-=20',
y: "-=20",
yoyo: true,
repeat: -1,
duration: 1000,

View File

@@ -74,26 +74,26 @@ export const Nav00DataSource = {
// },
// ],
// },
{
name: "item1",
className: "header0-item",
children: {
href: "#",
children: [
{ children: i18n.t("landing.labels.features"), name: "text" },
],
},
},
{
name: "item2",
className: "header0-item",
children: {
href: "#",
children: [
{ children: i18n.t("landing.labels.pricing"), name: "text" },
],
},
},
// {
// name: "item1",
// className: "header0-item",
// children: {
// href: "#",
// children: [
// { children: i18n.t("landing.labels.features"), name: "text" },
// ],
// },
// },
// {
// name: "item2",
// className: "header0-item",
// children: {
// href: "#",
// children: [
// { children: i18n.t("landing.labels.pricing"), name: "text" },
// ],
// },
// },
{
name: "item3",
className: "header0-item",
@@ -124,7 +124,12 @@ export const Banner00DataSource = {
button: {
className: "banner0-button",
children: i18n.t("landing.hero.button"),
href: "https://imexsystems.ca",
href: "https://imexsystems.ca/products/imex-online",
},
button2: {
className: "banner0-button2",
children: i18n.t("landing.labels.managemyshop"),
href: "/signin",
},
};
export const Content40DataSource = {
@@ -1033,22 +1038,22 @@ export const Footer10DataSource = {
childWrapper: {
children: [
{
href: "/privacy",
href: "https://imexsystems.ca/privacy",
name: "link0",
children: i18n.t("landing.footer.company.privacypolicy"),
},
{
href: "/about",
name: "link1",
children: i18n.t("landing.footer.company.about"),
},
// {
// href: "/about",
// name: "link1",
// children: i18n.t("landing.footer.company.about"),
// },
{
href: "/disclaimer",
name: "link2",
children: i18n.t("landing.footer.company.disclaimers"),
},
{
href: "https://thinkimex.com",
href: "https://imexsystems.ca/schedule-a-demo/",
name: "link3",
children: i18n.t("landing.footer.company.contact"),
},
@@ -1063,7 +1068,7 @@ export const Footer10DataSource = {
className: "copyright",
children: (
<span>
©2021 <a href="http://imexsystems.ca">ImEX Systems</a> used under
©2022 <a href="http://imexsystems.ca">ImEX Systems</a> used under
license.
</span>
),

View File

@@ -4,27 +4,27 @@ import { enquireScreen } from "enquire-js";
import React from "react";
import Banner0 from "./Banner0";
// import Content4 from "./Content4";
import Content0 from "./Content0";
import Content1 from "./Content1";
//import Content0 from "./Content0";
//import Content1 from "./Content1";
import {
Banner00DataSource,
// Content40DataSource,
Content00DataSource,
Content10DataSource,
//Content00DataSource,
//Content10DataSource,
// Pricing11DataSource,
// Content30DataSource,
// Content120DataSource,
Footer10DataSource,
Nav00DataSource,
Pricing20DataSource,
// Nav00DataSource,
//Pricing20DataSource,
} from "./data.source";
// import Pricing1 from "./Pricing1";
// import Content3 from "./Content3";
// import Content12 from "./Content12";
import Footer1 from "./Footer1";
import "./less/antMotionStyle.less";
import Nav0 from "./Nav0";
import Pricing2 from "./Pricing2";
// import Nav0 from "./Nav0";
//import Pricing2 from "./Pricing2";
let isMobile;
enquireScreen((b) => {
@@ -62,64 +62,64 @@ export default class Home extends React.Component {
render() {
const children = [
<Nav0
id="Nav0_0"
key="Nav0_0"
dataSource={Nav00DataSource}
isMobile={this.state.isMobile}
/>,
// <Nav0
// id="Nav0_0"
// key="Nav0_0"
// dataSource={Nav00DataSource}
// isMobile={this.state.isMobile}
// />,
<Banner0
id="Banner0_0"
key="Banner0_0"
dataSource={Banner00DataSource}
isMobile={this.state.isMobile}
/>,
...(process.env.NODE_ENV !== "production"
? [
// <Content4
// id="Content4_0"
// key="Content4_0"
// dataSource={Content40DataSource}
// isMobile={this.state.isMobile}
// />,
<Content1
id="Content1_0"
key="Content1_0"
dataSource={Content10DataSource}
isMobile={this.state.isMobile}
/>,
<Content0
id="Content0_0"
key="Content0_0"
dataSource={Content00DataSource}
isMobile={this.state.isMobile}
/>,
<Pricing2
id="Pricing2_0"
key="Pricing2_0"
dataSource={Pricing20DataSource}
isMobile={this.state.isMobile}
/>,
// <Pricing1
// id="Pricing1_1"
// key="Pricing1_1"
// dataSource={Pricing11DataSource}
// isMobile={this.state.isMobile}
// />,
// <Content3
// id="Content3_0"
// key="Content3_0"
// dataSource={Content30DataSource}
// isMobile={this.state.isMobile}
// />,
// <Content12
// id="Content12_0"
// key="Content12_0"
// dataSource={Content120DataSource}
// isMobile={this.state.isMobile}
// />,
]
: []),
// ...(process.env.NODE_ENV !== "production"
// ? [
// // <Content4
// // id="Content4_0"
// // key="Content4_0"
// // dataSource={Content40DataSource}
// // isMobile={this.state.isMobile}
// // />,
// <Content1
// id="Content1_0"
// key="Content1_0"
// dataSource={Content10DataSource}
// isMobile={this.state.isMobile}
// />,
// <Content0
// id="Content0_0"
// key="Content0_0"
// dataSource={Content00DataSource}
// isMobile={this.state.isMobile}
// />,
// <Pricing2
// id="Pricing2_0"
// key="Pricing2_0"
// dataSource={Pricing20DataSource}
// isMobile={this.state.isMobile}
// />,
// // <Pricing1
// // id="Pricing1_1"
// // key="Pricing1_1"
// // dataSource={Pricing11DataSource}
// // isMobile={this.state.isMobile}
// // />,
// // <Content3
// // id="Content3_0"
// // key="Content3_0"
// // dataSource={Content30DataSource}
// // isMobile={this.state.isMobile}
// // />,
// // <Content12
// // id="Content12_0"
// // key="Content12_0"
// // dataSource={Content120DataSource}
// // isMobile={this.state.isMobile}
// // />,
// ]
// : []),
<Footer1
id="Footer1_0"
key="Footer1_0"

View File

@@ -6,7 +6,7 @@
position: relative;
text-align: center;
border-color: #666;
background-image: url("../../assets/banner1.jpeg");
background-image: url("../../assets/banner4.jpg");
background-size: cover;
background-attachment: fixed;
background-position: center;
@@ -60,6 +60,28 @@
width: auto;
}
}
& &-button2 {
border: 1px solid #fff;
color: #fff;
// background: transparent;
box-shadow: 0 0 0 transparent;
font-size: 16px;
height: 40px;
transition: background 0.45s @ease-out, box-shadow 0.45s @ease-out;
&:hover {
color: #fff;
border-color: #fff;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 10px rgba(50, 250, 255, 0.75);
}
&:focus {
color: #fff;
border-color: #fff;
}
&.queue-anim-leaving {
width: auto;
}
}
& &-icon {
bottom: 20px;
font-size: 24px;

View File

@@ -6,6 +6,7 @@ import Icon, {
PrinterFilled,
ToolFilled,
HistoryOutlined,
SyncOutlined,
} from "@ant-design/icons";
import {
Button,
@@ -178,6 +179,15 @@ export function JobsDetailPage({
const menuExtra = (
<Space wrap>
<Button
onClick={() => {
refetch();
}}
key="refresh"
>
<SyncOutlined />
{t("general.labels.refresh")}
</Button>
<JobsChangeStatus job={job} />
<JobSyncButton job={job} />
<Button

View File

@@ -0,0 +1,36 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import JobDetailCards from "../../components/job-detail-cards/job-detail-cards.component";
import JobsReadyList from "../../components/jobs-ready-list/jobs-ready-list.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function JobsReadyPage({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.readyjobs");
setSelectedHeader("readyjobs");
setBreadcrumbs([
{ link: "/manage/jobs", label: t("titles.bc.jobs-ready") },
]);
}, [t, setBreadcrumbs, setSelectedHeader]);
return (
<RbacWrapper action="jobs:list-ready">
<JobsReadyList />
<JobDetailCards />
</RbacWrapper>
);
}
export default connect(null, mapDispatchToProps)(JobsReadyPage);

View File

@@ -127,6 +127,7 @@ const AccountingPayments = lazy(() =>
import("../accounting-payments/accounting-payments.container")
);
const AllJobs = lazy(() => import("../jobs-all/jobs-all.container"));
const ReadyJobs = lazy(() => import("../jobs-ready/jobs-ready.page"));
const JobsClose = lazy(() => import("../jobs-close/jobs-close.container"));
const JobsAdmin = lazy(() => import("../jobs-admin/jobs-admin.page"));
const TempDocs = lazy(() =>
@@ -240,6 +241,7 @@ export function Manage({ match, conflict, bodyshop }) {
component={JobsAdmin}
/>
<Route exact path={`${match.path}/jobs/all`} component={AllJobs} />
<Route exact path={`${match.path}/jobs/ready`} component={ReadyJobs} />
<Route
exact
path={`${match.path}/jobs/new`}

View File

@@ -1,23 +1,22 @@
import { SyncOutlined } from "@ant-design/icons";
import { useQuery } from "@apollo/client";
import { Button, Card, Input, Space, Table } from "antd";
import _ from "lodash";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory } from "react-router-dom";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component";
import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { onlyUnique } from "../../utils/arrayHelper";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import { useLocation } from "react-router-dom";
import queryString from "query-string";
import _ from "lodash";
import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
import { alphaSort, dateSort } from "../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -25,20 +24,25 @@ const mapStateToProps = createStructuredSelector({
export function PartsQueuePageComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
const { page, sortcolumn, sortorder, statusFilters } = searchParams;
const {
//page,
sortcolumn,
sortorder,
statusFilters,
} = searchParams;
const history = useHistory();
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
offset: page ? (page - 1) * 25 : 0,
limit: 25,
// offset: page ? (page - 1) * 25 : 0,
// limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
order: [
{
[sortcolumn || "updated_at"]: sortorder
[sortcolumn || "ro_number"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
@@ -85,7 +89,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
: [];
const handleTableChange = (pagination, filters, sorter) => {
searchParams.page = pagination.current;
// searchParams.page = pagination.current;
searchParams.sortcolumn = sorter.columnKey;
searchParams.sortorder = sorter.order;
if (filters.status) {
@@ -114,10 +118,10 @@ export function PartsQueuePageComponent({ bodyshop }) {
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
// sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
// sortOrder: sortcolumn === "owner" && sortorder,
dataIndex: "ownr_ln",
key: "ownr_ln",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
<Link to={"/manage/owners/" + record.ownerid}>
@@ -173,16 +177,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
);
},
},
{
title: t("vehicles.fields.plate_no"),
dataIndex: "plate_no",
key: "plate_no",
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
sortOrder: sortcolumn === "plate_no" && sortorder,
render: (text, record) => {
return record.plate_no ? record.plate_no : "";
},
},
// {
// title: t("vehicles.fields.plate_no"),
// dataIndex: "plate_no",
// key: "plate_no",
// sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
// sortOrder: sortcolumn === "plate_no" && sortorder,
// render: (text, record) => {
// return record.plate_no ? record.plate_no : "";
// },
// },
{
title: t("jobs.fields.clm_no"),
dataIndex: "clm_no",
@@ -198,34 +202,49 @@ export function PartsQueuePageComponent({ bodyshop }) {
);
},
},
{
title: t("jobs.fields.clm_total"),
dataIndex: "clm_total",
key: "clm_total",
sorter: (a, b) => a.clm_total - b.clm_total,
sortOrder: sortcolumn === "clm_total" && sortorder,
render: (text, record) => {
return record.clm_total ? (
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
) : (
t("general.labels.unknown")
);
},
},
// {
// title: t("jobs.fields.clm_total"),
// dataIndex: "clm_total",
// key: "clm_total",
// sorter: (a, b) => a.clm_total - b.clm_total,
// sortOrder: sortcolumn === "clm_total" && sortorder,
// render: (text, record) => {
// return record.clm_total ? (
// <CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
// ) : (
// t("general.labels.unknown")
// );
// },
// },
{
title: t("jobs.fields.updated_at"),
dataIndex: "updated_at",
key: "updated_at",
sorter: (a, b) => dateSort(a.updated_at, b.updated_at),
sortOrder: sortcolumn === "updated_at" && sortorder,
render: (text, record) => (
<TimeAgoFormatter>{record.updated_at}</TimeAgoFormatter>
),
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
title: t("jobs.fields.partsstatus"),
dataIndex: "partsstatus",
key: "partsstatus",
render: (text, record) => (
<JobRemoveFromPartsQueue jobId={record.id} refetch={refetch} />
<JobPartsQueueCount parts={record.joblines_status} />
),
},
{
title: t("jobs.fields.queued_for_parts"),
dataIndex: "queued_for_parts",
key: "queued_for_parts",
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
render: (text, record) => (
<JobRemoveFromPartsQueue
checked={record.queued_for_parts}
jobId={record.id}
/>
),
},
];
@@ -253,9 +272,9 @@ export function PartsQueuePageComponent({ bodyshop }) {
loading={loading}
pagination={{
position: "top",
pageSize: 25,
current: parseInt(page || 1),
total: data && data.jobs_aggregate.aggregate.count,
pageSize: 50,
// current: parseInt(page || 1),
// total: data && data.jobs_aggregate.aggregate.count,
}}
columns={columns}
rowKey="id"

View File

@@ -90,6 +90,9 @@
},
"audit_trail": {
"messages": {
"admin_jobmarkexported": "ADMIN: Job marked as exported.",
"admin_jobmarkforreexport": "ADMIN: Job marked for re-export.",
"admin_jobunvoid": "ADMIN: Job has been unvoided.",
"billposted": "Bill with invoice number {{invoice_number}} posted.",
"billupdated": "Bill with invoice number {{invoice_number}} updated.",
"jobassignmentchange": "Employee {{name}} assigned to {{operation}}",
@@ -171,7 +174,8 @@
"bill_total": "Bill Total Amount",
"billcmtotal": "Credit Memos",
"bills": "Bills",
"creditsnotreceived": "Credits Not Received",
"calculatedcreditsnotreceived": "Calculated CNR",
"creditsnotreceived": "Credits Not Marked Received",
"creditsreceived": "Credits Received",
"dedfromlbr": "Labor Adjustments",
"deleteconfirm": "Are you sure you want to delete this bill? It cannot be undone. If this bill has deductions from labors, manual changes may be required.",
@@ -355,6 +359,7 @@
"intake": "Jobs -> Intake",
"list-active": "Jobs -> List Active",
"list-all": "Jobs -> List All",
"list-ready": "Jobs -> List Ready",
"partsqueue": "Jobs -> Parts Queue"
},
"owners": {
@@ -476,6 +481,7 @@
"status": "Status Label",
"statuses": {
"active_statuses": "Active Statuses (Filtering for Active Jobs throughout system)",
"additional_board_statuses": "Additional Status to Display on Production Board",
"color": "Color",
"default_arrived": "Default Arrived Status (Transition to Production)",
"default_bo": "Default Backordered Status",
@@ -494,7 +500,8 @@
"post_production_statuses": "Post-Production Statuses",
"pre_production_statuses": "Pre-Production Statuses",
"production_colors": "Production Status Colors",
"production_statuses": "Production Statuses"
"production_statuses": "Production Statuses",
"ready_statuses": "Ready Statuses"
},
"target_touchtime": "Target Touch Time",
"timezone": "Timezone",
@@ -905,7 +912,8 @@
"createdat": "Created At"
},
"labels": {
"attempts": "Export Attempts"
"attempts": "Export Attempts",
"priorsuccesfulexport": "This record has previously been exported successfully. Please make sure it has already been deleted in the target system."
}
},
"general": {
@@ -987,6 +995,7 @@
"passwordresetvalidatesuccess_sub": "You may now sign in again using your new password. ",
"passwordsdonotmatch": "The passwords you have entered do not match.",
"print": "Print",
"refresh": "Refresh",
"required": "Required",
"saturday": "Saturday",
"search": "Search...",
@@ -1385,6 +1394,7 @@
"prt_tax_rt": "Part Tax Rate",
"prt_type": "Part Type"
},
"partsstatus": "Parts Status",
"pas": "Sublet",
"pay_date": "Pay Date",
"phoneshort": "PH",
@@ -1394,6 +1404,7 @@
"production_vars": {
"note": "Production Note"
},
"queued_for_parts": "Queued for Parts",
"rate_ats": "ATS Rate",
"rate_la1": "LA1",
"rate_la2": "LA2",
@@ -1579,14 +1590,15 @@
"partstotal": "Parts Total (ex. Taxes)",
"plitooltips": {
"billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).",
"creditmemos": "The total amount of all returns created. This amount does not reflect credit memos that have been posted.",
"creditsnotreceived": "The total amount of returns created for this job that do not have a corresponding credit memo posted. An amount greater than $0 indicates that vendors have not provided requested credit memos.",
"calculatedcreditsnotreceived": "The calculated credits not received is derived by subtracting the amount of credit memos entered from the <b>retail</b> total of returns created. This does not take into account whether the credit was marked as received. You can find more information <a href=\"https://help.imex.online/en/article/credits-not-received-changes-1jy9snw\" target=\"_blank\">here</a>.",
"creditmemos": "The total <b>retail</b> amount of all returns created. This amount does not reflect credit memos that have been posted.",
"creditsnotreceived": "This total reflects the total <b>retail</b> of parts returns lines that have not been explicitly marked as returned when posting a credit memo. You can learn more about this here <a href=\"https://help.imex.online/en/article/credits-not-received-changes-1jy9snw\" target=\"_blank\">here</a>. ",
"discrep1": "If the discrepancy is not $0, you may have one of the following: <br/><br/>\n\n<ul>\n<li>Too many bills/bill lines that have been posted against this RO. Check to make sure every bill posted on this RO is correctly posted and assigned.</li>\n<li>You do not have the latest supplement imported, or, a supplement must be submitted and then imported.</li>\n<li>You have posted a bill line to labor.</li>\n</ul>\n<br/>\n<i>There may be additional issues not listed above that prevent this job from reconciling.</i>",
"discrep2": "If the discrepancy is not $0, you may have one of the following: <br/><br/>\n\n<ul>\n<li>Used an incorrect rate when deducting from labor.</li>\n<li>An outstanding imbalance higher in the reconciliation process.</li>\n</ul>\n<br/>\n<i>There may be additional issues not listed above that prevent this job from reconciling.</i>",
"discrep3": "If the discrepancy is not $0, you may have one of the following: <br/><br/>\n\n<ul>\n<li>Credit memos that have not been received or posted.</li>\n<li>An outstanding imbalance higher in the reconciliation process.</li>\n</ul>\n<br/>\n<i>There may be additional issues not listed above that prevent this job from reconciling.</i>",
"discrep3": "If the discrepancy is not $0, you may have one of the following: <br/><br/>\n\n<ul>\n<li>A parts order return has not been created.</li>\n<li>An outstanding imbalance higher in the reconciliation process.</li>\n</ul>\n<br/>\n<i>There may be additional issues not listed above that prevent this job from reconciling.</i>",
"laboradj": "The sum of all bill lines that deducted from labor hours, rather than part prices.",
"partstotal": "This is the total of all parts and sublet amounts on the vehicle (some of these may require an in-house invoice).<br/>\nItems such as shop and paint materials, labor online lines, etc. are not included in this total.",
"totalreturns": "The total amount of returns created for this job."
"totalreturns": "The total <b>retail</b> amount of returns created for this job."
},
"prt_dsmk_total": "Line Item Adjustment",
"rates": "Rates",
@@ -1673,15 +1685,15 @@
"name": "ImEX Online",
"status": "System Status"
},
"slogan": "A whole new kind of shop management system."
"slogan": "ImEX Systems Inc. is a technology leader in the collision repair industry. We specialize in creating collision repair management systems and bodyshop management systems that lower cycle times and promote efficiency."
},
"hero": {
"button": "Learn More",
"title": "A whole new kind of shop management system."
"title": "Shop management reimagined."
},
"labels": {
"features": "Features",
"managemyshop": "Manage my Shop",
"managemyshop": "Sign In",
"pricing": "Pricing"
},
"pricing": {
@@ -1740,6 +1752,7 @@
"phonebook": "Phonebook",
"productionboard": "Production Board - Visual",
"productionlist": "Production Board - List",
"readyjobs": "Ready Jobs",
"recent": "Recent Items",
"reportcenter": "Report Center",
"rescueme": "Rescue me!",
@@ -1904,7 +1917,8 @@
},
"parts": {
"actions": {
"order": "Order Parts"
"order": "Order Parts",
"orderinhouse": "Order as In House"
}
},
"parts_orders": {
@@ -1917,12 +1931,15 @@
"associatedbills": "This parts order cannot",
"backordering": "Error backordering part {{message}}.",
"creating": "Error encountered when creating parts order. ",
"oec": "Error creating EMS files for OEC. {{error}}"
"oec": "Error creating EMS files for OEC. {{error}}",
"saving": "Error saving parts order. {{error}}.",
"updating": "Error updating parts order/parts order line. {{error}}."
},
"fields": {
"act_price": "Price",
"backordered_eta": "B.O. ETA",
"backordered_on": "B.O. On",
"cm_received": "CM Received?",
"comments": "Comments",
"cost": "Cost",
"db_price": "List Price",
@@ -1945,6 +1962,7 @@
"confirmdelete": "Are you sure you want to delete this item? It cannot be recovered. Job line statuses will not be updated and may require manual review. ",
"email": "Send by Email",
"inthisorder": "Parts in this Order",
"mark_as_received": "Mark as Received?",
"newpartsorder": "New Parts Order",
"notyetordered": "This part has not yet been ordered.",
"oec": "Order via OEC",
@@ -1956,6 +1974,7 @@
},
"successes": {
"created": "Parts order created successfully. ",
"line_updated": "Parts return line updated.",
"received": "Parts order received.",
"return_created": "Parts return created successfully."
}
@@ -2077,6 +2096,7 @@
"estimate_detail": "Estimate Details",
"estimate_followup": "Estimate Followup",
"express_repair_checklist": "Express Repair Checklist",
"filing_coversheet_landscape": "Filing Coversheet (Landscape)",
"filing_coversheet_portrait": "Filing Coversheet (Portrait)",
"final_invoice": "Final Invoice",
"fippa_authorization": "FIPPA Authorization",
@@ -2182,6 +2202,7 @@
"settings": "Error saving board settings: {{error}}"
},
"labels": {
"actual_in": "Actual In",
"alert": "Alert",
"alertoff": "Remove alert from job",
"alerton": "Add alert to job",
@@ -2207,6 +2228,7 @@
"note": "Production Note",
"ownr_nm": "Owner Name",
"paintpriority": "P/P",
"partsstatus": "Parts Status",
"production_note": "Production Note",
"refinishhours": "R",
"scheduled_completion": "Scheduled Completion",
@@ -2494,6 +2516,7 @@
"jobs-detail": "Job {{number}}",
"jobs-intake": "Intake",
"jobs-new": "Create a New Job",
"jobs-ready": "Ready Jobs",
"owner-detail": "{{name}}",
"owners": "Owners",
"parts-queue": "Parts Queue",
@@ -2543,6 +2566,7 @@
"productionboard": "Production - Board",
"productionlist": "Production Board - List | $t(titles.app)",
"profile": "My Profile | $t(titles.app)",
"readyjobs": "Ready Jobs | $t(titles.app)",
"resetpassword": "Reset Password",
"resetpasswordvalidate": "Enter New Password",
"schedule": "Schedule | $t(titles.app)",

View File

@@ -90,6 +90,9 @@
},
"audit_trail": {
"messages": {
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobunvoid": "",
"billposted": "",
"billupdated": "",
"jobassignmentchange": "",
@@ -171,6 +174,7 @@
"bill_total": "",
"billcmtotal": "",
"bills": "",
"calculatedcreditsnotreceived": "",
"creditsnotreceived": "",
"creditsreceived": "",
"dedfromlbr": "",
@@ -355,6 +359,7 @@
"intake": "",
"list-active": "",
"list-all": "",
"list-ready": "",
"partsqueue": ""
},
"owners": {
@@ -476,6 +481,7 @@
"status": "",
"statuses": {
"active_statuses": "",
"additional_board_statuses": "",
"color": "",
"default_arrived": "",
"default_bo": "",
@@ -494,7 +500,8 @@
"post_production_statuses": "",
"pre_production_statuses": "",
"production_colors": "",
"production_statuses": ""
"production_statuses": "",
"ready_statuses": ""
},
"target_touchtime": "",
"timezone": "",
@@ -905,7 +912,8 @@
"createdat": ""
},
"labels": {
"attempts": ""
"attempts": "",
"priorsuccesfulexport": ""
}
},
"general": {
@@ -987,6 +995,7 @@
"passwordresetvalidatesuccess_sub": "",
"passwordsdonotmatch": "",
"print": "",
"refresh": "",
"required": "",
"saturday": "",
"search": "Buscar...",
@@ -1385,6 +1394,7 @@
"prt_tax_rt": "",
"prt_type": ""
},
"partsstatus": "",
"pas": "",
"pay_date": "Fecha de Pay",
"phoneshort": "PH",
@@ -1394,6 +1404,7 @@
"production_vars": {
"note": ""
},
"queued_for_parts": "",
"rate_ats": "",
"rate_la1": "Tarifa LA1",
"rate_la2": "Tarifa LA2",
@@ -1579,6 +1590,7 @@
"partstotal": "",
"plitooltips": {
"billtotal": "",
"calculatedcreditsnotreceived": "",
"creditmemos": "",
"creditsnotreceived": "",
"discrep1": "",
@@ -1740,6 +1752,7 @@
"phonebook": "",
"productionboard": "",
"productionlist": "",
"readyjobs": "",
"recent": "",
"reportcenter": "",
"rescueme": "",
@@ -1904,7 +1917,8 @@
},
"parts": {
"actions": {
"order": "Pedido de piezas"
"order": "Pedido de piezas",
"orderinhouse": ""
}
},
"parts_orders": {
@@ -1917,12 +1931,15 @@
"associatedbills": "",
"backordering": "",
"creating": "Se encontró un error al crear el pedido de piezas.",
"oec": ""
"oec": "",
"saving": "",
"updating": ""
},
"fields": {
"act_price": "",
"backordered_eta": "",
"backordered_on": "",
"cm_received": "",
"comments": "",
"cost": "",
"db_price": "",
@@ -1945,6 +1962,7 @@
"confirmdelete": "",
"email": "Enviar por correo electrónico",
"inthisorder": "Partes en este pedido",
"mark_as_received": "",
"newpartsorder": "",
"notyetordered": "",
"oec": "",
@@ -1956,6 +1974,7 @@
},
"successes": {
"created": "Pedido de piezas creado con éxito.",
"line_updated": "",
"received": "",
"return_created": ""
}
@@ -2077,6 +2096,7 @@
"estimate_detail": "",
"estimate_followup": "",
"express_repair_checklist": "",
"filing_coversheet_landscape": "",
"filing_coversheet_portrait": "",
"final_invoice": "",
"fippa_authorization": "",
@@ -2182,6 +2202,7 @@
"settings": ""
},
"labels": {
"actual_in": "",
"alert": "",
"alertoff": "",
"alerton": "",
@@ -2207,6 +2228,7 @@
"note": "",
"ownr_nm": "",
"paintpriority": "",
"partsstatus": "",
"production_note": "",
"refinishhours": "",
"scheduled_completion": "",
@@ -2494,6 +2516,7 @@
"jobs-detail": "",
"jobs-intake": "",
"jobs-new": "",
"jobs-ready": "",
"owner-detail": "",
"owners": "",
"parts-queue": "",
@@ -2543,6 +2566,7 @@
"productionboard": "",
"productionlist": "",
"profile": "Mi perfil | $t(titles.app)",
"readyjobs": "",
"resetpassword": "",
"resetpasswordvalidate": "",
"schedule": "Horario | $t(titles.app)",

View File

@@ -90,6 +90,9 @@
},
"audit_trail": {
"messages": {
"admin_jobmarkexported": "",
"admin_jobmarkforreexport": "",
"admin_jobunvoid": "",
"billposted": "",
"billupdated": "",
"jobassignmentchange": "",
@@ -171,6 +174,7 @@
"bill_total": "",
"billcmtotal": "",
"bills": "",
"calculatedcreditsnotreceived": "",
"creditsnotreceived": "",
"creditsreceived": "",
"dedfromlbr": "",
@@ -355,6 +359,7 @@
"intake": "",
"list-active": "",
"list-all": "",
"list-ready": "",
"partsqueue": ""
},
"owners": {
@@ -476,6 +481,7 @@
"status": "",
"statuses": {
"active_statuses": "",
"additional_board_statuses": "",
"color": "",
"default_arrived": "",
"default_bo": "",
@@ -494,7 +500,8 @@
"post_production_statuses": "",
"pre_production_statuses": "",
"production_colors": "",
"production_statuses": ""
"production_statuses": "",
"ready_statuses": ""
},
"target_touchtime": "",
"timezone": "",
@@ -905,7 +912,8 @@
"createdat": ""
},
"labels": {
"attempts": ""
"attempts": "",
"priorsuccesfulexport": ""
}
},
"general": {
@@ -987,6 +995,7 @@
"passwordresetvalidatesuccess_sub": "",
"passwordsdonotmatch": "",
"print": "",
"refresh": "",
"required": "",
"saturday": "",
"search": "Chercher...",
@@ -1385,6 +1394,7 @@
"prt_tax_rt": "",
"prt_type": ""
},
"partsstatus": "",
"pas": "",
"pay_date": "Date d'Pay",
"phoneshort": "PH",
@@ -1394,6 +1404,7 @@
"production_vars": {
"note": ""
},
"queued_for_parts": "",
"rate_ats": "",
"rate_la1": "Taux LA1",
"rate_la2": "Taux LA2",
@@ -1579,6 +1590,7 @@
"partstotal": "",
"plitooltips": {
"billtotal": "",
"calculatedcreditsnotreceived": "",
"creditmemos": "",
"creditsnotreceived": "",
"discrep1": "",
@@ -1740,6 +1752,7 @@
"phonebook": "",
"productionboard": "",
"productionlist": "",
"readyjobs": "",
"recent": "",
"reportcenter": "",
"rescueme": "",
@@ -1904,7 +1917,8 @@
},
"parts": {
"actions": {
"order": "Commander des pièces"
"order": "Commander des pièces",
"orderinhouse": ""
}
},
"parts_orders": {
@@ -1917,12 +1931,15 @@
"associatedbills": "",
"backordering": "",
"creating": "Erreur rencontrée lors de la création de la commande de pièces.",
"oec": ""
"oec": "",
"saving": "",
"updating": ""
},
"fields": {
"act_price": "",
"backordered_eta": "",
"backordered_on": "",
"cm_received": "",
"comments": "",
"cost": "",
"db_price": "",
@@ -1945,6 +1962,7 @@
"confirmdelete": "",
"email": "Envoyé par email",
"inthisorder": "Pièces dans cette commande",
"mark_as_received": "",
"newpartsorder": "",
"notyetordered": "",
"oec": "",
@@ -1956,6 +1974,7 @@
},
"successes": {
"created": "Commande de pièces créée avec succès.",
"line_updated": "",
"received": "",
"return_created": ""
}
@@ -2077,6 +2096,7 @@
"estimate_detail": "",
"estimate_followup": "",
"express_repair_checklist": "",
"filing_coversheet_landscape": "",
"filing_coversheet_portrait": "",
"final_invoice": "",
"fippa_authorization": "",
@@ -2182,6 +2202,7 @@
"settings": ""
},
"labels": {
"actual_in": "",
"alert": "",
"alertoff": "",
"alerton": "",
@@ -2207,6 +2228,7 @@
"note": "",
"ownr_nm": "",
"paintpriority": "",
"partsstatus": "",
"production_note": "",
"refinishhours": "",
"scheduled_completion": "",
@@ -2494,6 +2516,7 @@
"jobs-detail": "",
"jobs-intake": "",
"jobs-new": "",
"jobs-ready": "",
"owner-detail": "",
"owners": "",
"parts-queue": "",
@@ -2543,6 +2566,7 @@
"productionboard": "",
"productionlist": "",
"profile": "Mon profil | $t(titles.app)",
"readyjobs": "",
"resetpassword": "",
"resetpasswordvalidate": "",
"schedule": "Horaire | $t(titles.app)",

View File

@@ -3,12 +3,17 @@ import i18n from "i18next";
const AuditTrailMapping = {
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", { status }),
admin_jobstatuschange: (status) =>
"ADMIN: " + i18n.t("audit_trail.messages.jobstatuschange", { status }),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
jobconverted: (ro_number) =>
i18n.t("audit_trail.messages.jobconverted", { ro_number }),
jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
admin_jobfieldchange: (field, value) =>
"ADMIN: " +
i18n.t("audit_trail.messages.jobfieldchanged", { field, value }),
jobspartsorder: (order_number) =>
i18n.t("audit_trail.messages.jobspartsorder", { order_number }),
jobspartsreturn: (order_number) =>
@@ -29,6 +34,11 @@ const AuditTrailMapping = {
jobnoteadded: () => i18n.t("audit_trail.messages.jobnoteadded"),
jobnoteupdated: () => i18n.t("audit_trail.messages.jobnoteupdated"),
jobnotedeleted: () => i18n.t("audit_trail.messages.jobnotedeleted"),
admin_jobunvoid: () => i18n.t("audit_trail.messages.admin_jobunvoid"),
admin_jobmarkforreexport: () =>
i18n.t("audit_trail.messages.admin_jobmarkforreexport"),
admin_jobmarkexported: () =>
i18n.t("audit_trail.messages.admin_jobmarkexported"),
};
export default AuditTrailMapping;

View File

@@ -1,7 +1,7 @@
import { gql } from "@apollo/client";
import { notification } from "antd";
import axios from "axios";
import jsreport from "@jsreport/browser-client";
import jsreport from "jsreport-browser-client-dist";
import _ from "lodash";
import moment from "moment";
import { auth } from "../firebase/firebase.utils";
@@ -24,6 +24,7 @@ export default async function RenderTemplate(
let { contextData, useShopSpecificTemplate } = await fetchContextData(
templateObject
);
console.log(templateObject.name);
const { ignoreCustomMargins } = Templates[templateObject.name];
let reportRequest = {
@@ -46,9 +47,9 @@ export default async function RenderTemplate(
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 36
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "36px",
: "50px",
},
}),
}),
@@ -59,6 +60,7 @@ export default async function RenderTemplate(
...templateObject.variables,
...templateObject.context,
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
offset: bodyshop.timezone, //moment().utcOffset(),
},
@@ -156,24 +158,49 @@ export async function RenderTemplates(
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 36
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "36px",
: "50px",
},
}),
pdfOperations: templateAndData.map((template) => {
return {
pdfOperations: [
{
template: {
name: template.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
: `/${template.templateObject.name}`,
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
name: "/components/Header-Footer",
recipe: "chrome-pdf",
engine: "handlebars",
},
type: "append",
mergeWholeDocument: true,
renderForEveryPage: true,
};
}),
type: "merge",
},
...templateAndData.map((template) => {
return {
template: {
chrome: {
marginTop:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin
: "36px",
marginBottom:
bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin
: "50px",
},
name: template.useShopSpecificTemplate
? `/${bodyshop.imexshopid}/${template.templateObject.name}`
: `/${template.templateObject.name}`,
...(renderAsHtml ? {} : { recipe: "chrome-pdf" }),
},
type: "append",
// mergeWholeDocument: true,
// renderForEveryPage: true,
};
}),
],
},
data: {
...extend(
@@ -184,6 +211,7 @@ export async function RenderTemplates(
// ...rootTemplate.templateObject.variables,
// ...rootTemplate.templateObject.context,
headerpath: `/${bodyshop.imexshopid}/header.html`,
footerpath: `/${bodyshop.imexshopid}/footer.html`,
bodyshop: bodyshop,
offset: moment().utcOffset(),
},

View File

@@ -314,6 +314,14 @@ export const TemplateList = (type, context) => {
disabled: false,
group: "post",
},
filing_coversheet_landscape: {
title: i18n.t("printcenter.jobs.filing_coversheet_landscape"),
description: "CASL Authorization",
subject: i18n.t("printcenter.jobs.filing_coversheet_landscape"),
key: "filing_coversheet_landscape",
disabled: false,
group: "post",
},
qc_sheet: {
title: i18n.t("printcenter.jobs.qc_sheet"),
description: "All Jobs Notes",
@@ -476,7 +484,7 @@ export const TemplateList = (type, context) => {
: {}),
...(!type || type === "job_special"
? {
thirdpartypayer: {
special_thirdpartypayer: {
title: i18n.t("printcenter.jobs.thirdpartypayer"),
description: "CSI invite",
key: "special_thirdpartypayer",

File diff suppressed because it is too large Load Diff

View File

@@ -779,6 +779,13 @@
table:
schema: public
name: timetickets
- name: transitions
using:
foreign_key_constraint_on:
column: bodyshopid
table:
schema: public
name: transitions
- name: vehicles
using:
foreign_key_constraint_on:
@@ -2596,6 +2603,13 @@
insertion_order: null
column_mapping:
id: jobid
- name: mixdata
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: mixdata
- name: notes
using:
foreign_key_constraint_on:
@@ -2645,6 +2659,13 @@
table:
schema: public
name: timetickets
- name: transitions
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: transitions
insert_permissions:
- role: user
permission:
@@ -3590,6 +3611,84 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: mixdata
object_relationships:
- name: job
using:
foreign_key_constraint_on: jobid
insert_permissions:
- role: user
permission:
check:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
columns:
- mixdata
- totalliquidcost
- totalsundrycost
- company
- version
- created_at
- updated_at
- id
- jobid
backend_only: false
select_permissions:
- role: user
permission:
columns:
- mixdata
- totalliquidcost
- totalsundrycost
- company
- version
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- mixdata
- totalliquidcost
- totalsundrycost
- company
- version
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
check: null
- table:
schema: public
name: notes
@@ -3839,6 +3938,7 @@
- act_price
- backordered_eta
- backordered_on
- cm_received
- cost
- created_at
- db_price
@@ -3859,6 +3959,7 @@
- act_price
- backordered_eta
- backordered_on
- cm_received
- cost
- created_at
- db_price
@@ -3890,6 +3991,7 @@
- act_price
- backordered_eta
- backordered_on
- cm_received
- cost
- created_at
- db_price
@@ -4594,6 +4696,93 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: transitions
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: job
using:
foreign_key_constraint_on: jobid
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
columns:
- bodyshopid
- created_at
- duration
- end
- id
- jobid
- next_value
- prev_value
- start
- type
- updated_at
- value
backend_only: false
select_permissions:
- role: user
permission:
columns:
- duration
- next_value
- prev_value
- type
- value
- created_at
- end
- start
- updated_at
- bodyshopid
- id
- jobid
filter:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- duration
- next_value
- prev_value
- type
- value
- created_at
- end
- start
- updated_at
- bodyshopid
- id
- jobid
filter:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
check: {}
- table:
schema: public
name: users

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"."parts_order_lines" add column "cm_received" boolean
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."parts_order_lines" add column "cm_received" boolean
null;

View File

@@ -0,0 +1,5 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE INDEX idx_pol_cm_received ON parts_order_lines(cm_received);
-- CREATE INDEX idx_pol_orderid ON parts_order_lines(orderid);
-- CREATE INDEX idx_parts_order_jobid ON parts_orders(jobid);

View File

@@ -0,0 +1,3 @@
CREATE INDEX idx_pol_cm_received ON parts_order_lines(cm_received);
CREATE INDEX idx_pol_orderid ON parts_order_lines(orderid);
CREATE INDEX idx_parts_order_jobid ON parts_orders(jobid);

View File

@@ -0,0 +1,10 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE VIEW "public"."joblines_status" AS
-- SELECT j.jobid,
-- j.status,
-- count(1) AS count,
-- j.part_type
-- FROM joblines j
-- WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.removed IS false))
-- GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1,8 @@
CREATE OR REPLACE VIEW "public"."joblines_status" AS
SELECT j.jobid,
j.status,
count(1) AS count,
j.part_type
FROM joblines j
WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.removed IS false))
GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1,10 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- CREATE OR REPLACE VIEW "public"."joblines_status" AS
-- SELECT j.jobid,
-- j.status,
-- count(1) AS count,
-- j.part_type
-- FROM joblines j
-- WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.removed IS false))
-- GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1,8 @@
CREATE OR REPLACE VIEW "public"."joblines_status" AS
SELECT j.jobid,
j.status,
count(1) AS count,
j.part_type
FROM joblines j
WHERE ((j.part_type IS NOT NULL) AND (j.part_type <> 'PAE'::text) AND (j.removed IS false))
GROUP BY j.jobid, j.status, j.part_type;

View File

@@ -0,0 +1 @@
DROP TABLE "public"."transitions";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."transitions" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshoipid" uuid NOT NULL, "start" timestamptz NOT NULL, "end" timestamptz, "duration" numeric DEFAULT 0, "prev_value" text, "value" text, "next_value" Text, "jobid" uuid, "type" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshoipid") REFERENCES "public"."bodyshops"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_transitions_updated_at"
BEFORE UPDATE ON "public"."transitions"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_transitions_updated_at" ON "public"."transitions"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
alter table "public"."transitions" rename column "bodyshopid" to "bodyshoipid";

View File

@@ -0,0 +1 @@
alter table "public"."transitions" rename column "bodyshoipid" to "bodyshopid";

View File

@@ -0,0 +1 @@
DROP TABLE "public"."mixdata";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."mixdata" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "jobid" uuid NOT NULL, "company" text NOT NULL, "version" text NOT NULL, "totalliquidcost" numeric NOT NULL, "totalsundrycost" numeric NOT NULL, "mixdata" jsonb, PRIMARY KEY ("id") , FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_mixdata_updated_at"
BEFORE UPDATE ON "public"."mixdata"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_mixdata_updated_at" ON "public"."mixdata"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

5325
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
"version": "0.0.1",
"license": "UNLICENSED",
"engines": {
"node": "16.14.2",
"npm": "8.6.0"
"node": "12.22.6",
"npm": "7.17.0"
},
"scripts": {
"setup": "yarn && cd client && yarn",
@@ -17,39 +17,41 @@
"start": "node server.js"
},
"dependencies": {
"aws-sdk": "^2.1043.0",
"aws-sdk": "^2.1116.0",
"axios": "^0.24.0",
"bluebird": "^3.7.2",
"body-parser": "^1.18.3",
"cloudinary": "^1.27.1",
"body-parser": "^1.20.0",
"cloudinary": "^1.29.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "10.0.0",
"express": "^4.16.4",
"firebase-admin": "^10.0.0",
"graphql": "^16.0.1",
"express": "^4.17.3",
"firebase-admin": "^10.0.2",
"graphql": "^16.3.0",
"graphql-request": "^3.7.0",
"graylog2": "^0.2.1",
"inline-css": "^3.0.0",
"intuit-oauth": "^4.0.0",
"json-2-csv": "^3.17.0",
"json-2-csv": "^3.17.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"node-mailjet": "^3.3.4",
"multer": "^1.4.4",
"node-mailjet": "^3.3.10",
"node-quickbooks": "^2.0.39",
"nodemailer": "^6.7.1",
"phone": "^3.1.10",
"query-string": "^7.0.1",
"nodemailer": "^6.7.3",
"phone": "^3.1.15",
"query-string": "^7.1.1",
"soap": "^0.43.0",
"socket.io": "^4.4.0",
"ssh2-sftp-client": "^7.1.0",
"stripe": "^8.191.0",
"twilio": "^3.71.3",
"socket.io": "^4.4.1",
"ssh2-sftp-client": "^8.0.0",
"stripe": "^8.217.0",
"twilio": "^3.76.1",
"uuid": "^8.3.2",
"xml2js": "^0.4.23",
"xmlbuilder2": "^3.0.2"
},
"devDependencies": {

View File

@@ -7,7 +7,8 @@ const twilio = require("twilio");
const logger = require("./server/utils/logger");
var fb = require("./server/firebase/firebase-handler");
var cookieParser = require("cookie-parser");
const multer = require("multer");
const upload = multer();
//var enforce = require("express-sslify");
require("dotenv").config({
@@ -123,6 +124,11 @@ app.post("/sms/markConversationRead", smsStatus.markConversationRead);
var job = require("./server/job/job");
app.post("/job/totals", fb.validateFirebaseIdToken, job.totals);
app.post(
"/job/statustransition",
fb.validateFirebaseIdToken,
job.statustransition
);
app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu);
app.post("/job/costing", fb.validateFirebaseIdToken, job.costing);
app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti);
@@ -181,6 +187,15 @@ app.post("/data/arms", data.arms);
var taskHandler = require("./server/tasks/tasks");
app.post("/taskHandler", taskHandler.taskHandler);
var mixdataUpload = require("./server/mixdata/mixdata");
app.post(
"/mixdata/upload",
fb.validateFirebaseIdToken,
upload.any(),
mixdataUpload.mixdataUpload
);
var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default);
app.post("/newlog", (req, res) => {

View File

@@ -198,6 +198,8 @@ async function QueryJobData(socket, jobid) {
async function QueryVehicleFromDms(socket) {
try {
if (!socket.JobData.v_vin) return null;
const { data: VehicleGetResponse, request } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{

View File

@@ -637,6 +637,13 @@ exports.default = function ({
});
}
if (!qbo && InvoiceLineAdd.length === 0) {
//Handle the scenario where there is a $0 sale invoice.
InvoiceLineAdd.push({
Desc: "No estimate lines.",
});
}
return InvoiceLineAdd;
};

View File

@@ -1489,3 +1489,49 @@ mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
}
}
`;
exports.QUERY_EXISTING_TRANSITION = `
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
insert_exportlog_one(object: $log) {
id
}
}
`;
exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $existingTransition: transitions_set_input!){
update_transitions(where:{jobid:{_eq:$jobid}, end:{_is_null:true
}}, _set:$existingTransition){
affected_rows
returning{
id
start
end
prev_value
next_value
value
}
}
}`;
exports.INSERT_NEW_TRANSITION = `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, $oldTransitionId: uuid, $duration: numeric) {
insert_transitions_one(object: $newTransition) {
id
}
update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}
}
`;
exports.QUERY_JOB_ID_MIXDATA = `query QUERY_JOB_ID_MIXDATA($roNumbers: [String!]!) {
jobs(where: {ro_number: {_in: $roNumbers}}) {
id
ro_number
mixdata {
id
}
}
}
`;

View File

@@ -0,0 +1,84 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
//const client = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
async function StatusTransition(req, res) {
const { jobid, value, bodyshopid } = req.body;
const BearerToken = req.headers.authorization;
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
const { update_transitions } = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.UPDATE_OLD_TRANSITION, {
jobid: jobid,
existingTransition: {
end: new Date(),
next_value: value,
//duration
},
});
let duration =
update_transitions.affected_rows === 0
? 0
: new Date(update_transitions.returning[0].end) -
new Date(update_transitions.returning[0].start);
const resp2 = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_NEW_TRANSITION, {
oldTransitionId:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].id,
duration,
newTransition: {
bodyshopid: bodyshopid,
jobid: jobid,
start:
update_transitions.affected_rows === 0
? new Date()
: update_transitions.returning[0].end,
prev_value:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].value,
value: value,
type: "status",
},
});
//Check to see if there is an existing status transition record.
//Query using Job ID, start is not null, end is null.
//If there is no existing record, this is the start of the transition life cycle.
// Create the initial transition record.
//If there is a current status transition record, update it with the end date, duration, and next value.
res.sendStatus(200); //.json(ret);
} catch (error) {
logger.log("job-costing-error", "ERROR", req.user.email, jobid, {
message: error.message,
stack: error.stack,
});
res.status(400).send(JSON.stringify(error));
}
}
exports.statustransition = StatusTransition;

View File

@@ -2,3 +2,4 @@ exports.totals = require("./job-totals").default;
exports.totalsSsu = require("./job-totals").totalsSsu;
exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition;

View File

@@ -0,0 +1,206 @@
{
"PPG": {
"Header": {
"Protocol": {
"Message": "MixDataInterface",
"Name": "PPG",
"Version": "1.3.0"
},
"Transaction": {
"TransactionID": "3F2504E0-4F89-11D3-9A0C-0305E82C3301",
"TransactionDate": "2006-06-06T15:00:00"
},
"ShopInfo": {
"ShopID": "SomeShopID",
"ShopName": "Some Body Shop"
}
},
"DataExportInterface": {
"ROData": {
"ROCount": "2",
"RepairOrders": {
"RO": [
{
"ROCounter": "1",
"RONumber": "27187",
"Notes": "This is a painter note",
"Undercoat": "False",
"Clearcoat": "False",
"Basecoat": "True",
"TotalLiquidCost": "133.26",
"TotalSundryCost": "47.12",
"MixCount": "2",
"Mixes": {
"Mix": [
{
"MixCounter": "1",
"MixRONumber": "27187",
"MixedDate": "2008-09-17T00:00:00",
"MixedBy": "Tony Blair",
"MixedByEmployeeID": "5>",
"PPGBrandCode": "ABC",
"MixCost": "83.49",
"FormulaType": "Standard",
"ComponentCount": "5",
"Components": {
"Component": [
{
"ComponentCounter": "1",
"ComponentRONumber": "27187",
"ComponentCode": "P425-900",
"ComponentDescription": "F3550 CLEAR",
"ComponentCost": "44.84",
"ComponentWeightApplied": "521.5347",
"ComponentWeightTarget": "525.4023",
"ComponentDensity": "1.311"
},
{
"ComponentCounter": "2",
"ComponentRONumber": "27187",
"ComponentCode": "P425-948",
"ComponentDescription": "H/S BLACK",
"ComponentCost": "13.77",
"ComponentWeightApplied": "118.5779",
"ComponentWeightTarget": "118.5832",
"ComponentDensity": "0.971"
},
{
"ComponentCounter": "3",
"ComponentRONumber": "27187",
"ComponentCode": "P425-921",
"ComponentDescription": "H/S CLARET",
"ComponentCost": "0.24",
"ComponentWeightApplied": "2.082",
"ComponentWeightTarget": "2.1022",
"ComponentDensity": "0.976"
},
{
"ComponentCounter": "4",
"ComponentRONumber": "27187",
"ComponentCode": "P425-937",
"ComponentDescription": "PALE YELLOW",
"ComponentCost": "2.85",
"ComponentWeightApplied": "20.4412",
"ComponentWeightTarget": "20.4888",
"ComponentDensity": "1.136"
},
{
"ComponentCounter": "5",
"ComponentRONumber": "27187",
"ComponentCode": "P425-948",
"ComponentDescription": "Matting Agent",
"ComponentCost": "21.79",
"ComponentWeightApplied": "414.8808",
"ComponentWeightTarget": "414.8798",
"ComponentDensity": "0.987"
}
]
}
},
{
"MixCounter": "2",
"MixRONumber": "27187",
"MixedDate": "2008-09-17T00:00:00",
"MixedBy": "Bill Clinton",
"MixedByEmployeeID": "42>",
"PPGBrandCode": "DEF",
"MixCost": "49.76",
"FormulaType": "RFU",
"ComponentCount": "2",
"Components": {
"Component": [
{
"ComponentCounter": "1",
"ComponentRONumber": "27187",
"ComponentCode": "P850-1401",
"ComponentDescription": "Fade-Out Thinner",
"ComponentCost": "17.34",
"ComponentWeightApplied": "837.1649",
"ComponentWeightTarget": "838.2232",
"ComponentDensity": "0.8716"
},
{
"ComponentCounter": "2",
"ComponentRONumber": "27187",
"ComponentCode": "P190-478",
"ComponentDescription": "Ultraclear",
"ComponentCost": "32.42",
"ComponentWeightApplied": "862.5329",
"ComponentWeightTarget": "870.3238",
"ComponentDensity": "0.898"
}
]
}
}
]
},
"SundryCount": "1",
"Sundries": {
"Sundry": {
"SundryCounter": "1",
"SundryRONumber": "27187",
"SundryAddedDate": "2008-09-17T00:00:00",
"SundryAddedBy": "Tony Blair",
"SundryCode": "Sundry1",
"SundryDescription": "Bumper Prep Kit",
"SundryCost": "23.56",
"SundryQuantity": "2",
"SundryQuantityUOM": "Each"
}
}
},
{
"ROCounter": "2",
"RONumber": "27320",
"Notes": "This is a painter note too",
"Undercoat": "True",
"Clearcoat": "False",
"Basecoat": "False",
"TotalLiquidCost": "9.59",
"TotalSundryCost": "0.0",
"MixCount": "1",
"Mixes": {
"Mix": {
"MixCounter": "1",
"MixRONumber": "27320",
"MixedDate": "2008-12-03T00:00:00",
"MixedBy": "Jerry Primer",
"MixedByEmployeeID": "87>",
"PPGBrandCode": "GHI",
"MixCost": "9.59",
"FormulaType": "Standard",
"ComponentCount": "2",
"Components": {
"Component": [
{
"ComponentCounter": "1",
"ComponentRONumber": "27320",
"ComponentCode": "P565-957",
"ComponentDescription": "Long Life Etch",
"ComponentCost": "5.31",
"ComponentWeightApplied": "117.8754",
"ComponentWeightTarget": "116.0129",
"ComponentDensity": "0.9808"
},
{
"ComponentCounter": "2",
"ComponentRONumber": "27320",
"ComponentCode": "P275-61",
"ComponentDescription": "Acitvator For Long Life",
"ComponentCost": "4.28",
"ComponentWeightApplied": "103.4325",
"ComponentWeightTarget": "101.3950",
"ComponentDensity": "0.8571"
}
]
}
}
},
"SundryCount": "0"
}
]
}
}
}
}
}

144
server/mixdata/mixdata.js Normal file
View File

@@ -0,0 +1,144 @@
const path = require("path");
const _ = require("lodash");
const logger = require("../utils/logger");
const xml2js = require("xml2js");
const GraphQLClient = require("graphql-request").GraphQLClient;
const queries = require("../graphql-client/queries");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
exports.mixdataUpload = async (req, res) => {
const { bodyshopid } = req.body;
const BearerToken = req.headers.authorization;
logger.log("job-mixdata-upload", "DEBUG", req.user.email, null, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
req.files.forEach(async (element) => {
const b = Buffer.from(element.buffer);
console.log(b.toString());
const inboundRequest = await xml2js.parseStringPromise(b.toString(), {
explicitArray: false,
});
const ScaleType = DetermineScaleType(inboundRequest);
const RoNumbersFromInboundRequest = GetListOfRos(
inboundRequest,
ScaleType
);
if (RoNumbersFromInboundRequest.length > 0) {
//Query the list of ROs based on the RO number.
const { jobs } = await client.request(queries.QUERY_JOB_ID_MIXDATA, {
roNumbers: RoNumbersFromInboundRequest,
});
//Create the hash for faster processing for inserts/updates.
const jobHash = {};
jobs.forEach((j) => {
jobHash[j.ro_number] = {
jobid: j.id,
mixdataid: j.mixdata.length > 0 ? j.mixdata[0].id : null,
};
});
const MixDataArray = GenerateMixDataArray(
inboundRequest,
ScaleType,
jobHash
);
const MixDataQuery = `
mutation UPSERT_MIXDATA{
${MixDataArray.map((md, idx) =>
GenerateGqlForMixData(md, idx)
).join(" ")}
}
`;
const resp = await client.request(MixDataQuery);
//Process the list of ROs and return an object to generate the queries.
}
});
res.sendStatus(200);
} catch (error) {
res.status(500).JSON(error);
logger.log("job-mixdata-upload-error", "ERROR", null, null, {
error: error.message,
});
}
};
function DetermineScaleType(inboundRequest) {
const ret = { type: "", verson: 0 };
//PPG Mix Data
if (inboundRequest.PPG && inboundRequest.PPG.Header.Protocol.Name === "PPG") {
return {
type: inboundRequest.PPG.Header.Protocol.Name,
company: "PPG",
version: inboundRequest.PPG.Header.Protocol.Version,
};
}
}
function GetListOfRos(inboundRequest, ScaleType) {
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
return inboundRequest.PPG.DataExportInterface.ROData.RepairOrders.RO.map(
(r) => r.RONumber
);
}
}
function GenerateMixDataArray(inboundRequest, ScaleType, jobHash) {
if (ScaleType.company === "PPG" && ScaleType.version === "1.3.0") {
return inboundRequest.PPG.DataExportInterface.ROData.RepairOrders.RO.map(
(r) => {
return {
jobid: jobHash[r.RONumber].jobid,
id: jobHash[r.RONumber].mixdataid,
mixdata: r,
totalliquidcost: r.TotalLiquidCost,
totalsundrycost: r.TotalSundryCost,
company: ScaleType.company,
version: ScaleType.version,
};
}
);
}
}
function GenerateGqlForMixData(mixdata, key) {
const { id, ...restMixData } = mixdata;
if (id) {
//Update.
return `
update${key}: update_mixdata_by_pk(pk_columns:{id: "${id}"}, _set: ${JSON.stringify(
restMixData
).replace(/"(\w+)"\s*:/g, "$1:")}){
id
}
`;
} else {
//Insert
return `
insert${key}: insert_mixdata_one(object: ${JSON.stringify(
restMixData
).replace(/"(\w+)"\s*:/g, "$1:")}){
id
}
`;
}
}

620
yarn.lock

File diff suppressed because it is too large Load Diff