Compare commits

...

103 Commits

Author SHA1 Message Date
Patrick Fic
6aacce5a58 IO-233 Add refetch. 2021-09-17 10:22:20 -07:00
Patrick Fic
80dc04669c Merged in feature/cdk-cert (pull request #218)
feature/cdk-cert

Approved-by: Patrick Fic
2021-09-16 23:11:17 +00:00
Patrick Fic
a702c87986 io-233 Remote ITC Settings 2021-09-16 16:08:32 -07:00
Patrick Fic
fd161fa9eb IO-233 Add missing MAPA/MASH/Discount calulcations. 2021-09-15 08:28:54 -07:00
Patrick Fic
44a18f4ace Merged in feature/qbo (pull request #217)
feature/qbo

Approved-by: Patrick Fic
2021-09-14 21:10:15 +00:00
Patrick Fic
3269c4f602 Merged in feature/cdk-cert (pull request #216)
feature/cdk-cert

Approved-by: Patrick Fic
2021-09-14 21:09:10 +00:00
Patrick Fic
43cbf0084a Merged in release/2021-09-17 (pull request #215)
release/2021-09-17

Approved-by: Patrick Fic
2021-09-14 21:03:40 +00:00
Patrick Fic
28b1356f76 Merge branch 'release/2021-09-17' into feature/cdk-cert 2021-09-14 14:03:11 -07:00
Patrick Fic
38456cb213 Merge branch 'release/2021-09-17' into feature/qbo 2021-09-14 14:02:18 -07:00
Patrick Fic
8c826eaaed Update to latest node. 2021-09-14 14:01:55 -07:00
Patrick Fic
f047556ab5 Merge branch 'release/2021-09-17' into feature/qbo 2021-09-14 13:59:06 -07:00
Patrick Fic
057b335e82 Merge branch 'release/2021-09-17' into feature/cdk-cert 2021-09-14 13:58:48 -07:00
Patrick Fic
30c7da2bf9 Merged in hotfix-2021-09-14 (pull request #213)
IO-43 Resolve issue when no vehicle associated.

Approved-by: Patrick Fic
2021-09-14 16:41:07 +00:00
Patrick Fic
82fb4fc74c IO-43 Resolve issue when no vehicle associated. 2021-09-14 09:40:08 -07:00
Patrick Fic
151f122b8c Package updates. 2021-09-14 09:12:58 -07:00
Patrick Fic
b198ca5051 IO-233 Revert ITC Calculations. 2021-09-13 14:56:54 -07:00
Patrick Fic
d9abe84949 Merge release & add time ticket calculations. 2021-09-13 14:47:09 -07:00
Patrick Fic
17f4d69e30 Merged in release/2021-09-10 (pull request #211)
Resolve translation.

Approved-by: Patrick Fic
2021-09-13 17:09:49 +00:00
Patrick Fic
77d3fc359d Resolve translation. 2021-09-13 10:09:26 -07:00
Patrick Fic
86151f3337 Merge branch 'release/2021-09-10' into feature/cdk-cert 2021-09-13 10:08:15 -07:00
Patrick Fic
ae4b5bca33 Merged in release/2021-09-10 (pull request #210)
Add missing translations.

Approved-by: Patrick Fic
2021-09-13 16:50:13 +00:00
Patrick Fic
eb120a264e Add missing translations. 2021-09-13 09:49:38 -07:00
Patrick Fic
7e3f496ea1 IO-233 CDK WIP customer changes. 2021-09-13 09:43:41 -07:00
Patrick Fic
4d33f16f13 Merged in release/2021-09-10 (pull request #209)
release/2021-09-10

Approved-by: Patrick Fic
2021-09-13 15:09:19 +00:00
Patrick Fic
d7ebefe7ab Resolve missing query. 2021-09-13 08:08:43 -07:00
Patrick Fic
8dc2197677 IO-1353 Add missing default resp. centers. 2021-09-10 15:12:00 -07:00
Patrick Fic
fe993cba73 IO-233 CDK Reformatting. First successful post. 2021-09-10 14:58:09 -07:00
Patrick Fic
ef6cdf07d8 IO-1352 Remove positive hrs check on receivables export. 2021-09-10 13:09:21 -07:00
Patrick Fic
cdbde6f5fa Merged in release/2021-09-10 (pull request #207)
IO-43 Updated related ROs approach.

Approved-by: Patrick Fic
2021-09-09 22:35:46 +00:00
Patrick Fic
4059aa9875 IO-43 Updated related ROs approach. 2021-09-09 15:35:24 -07:00
Patrick Fic
22e30ae5cb IO-233 WIP CDK 2021-09-09 15:18:43 -07:00
Patrick Fic
78c883743f Merged in release/2021-09-10 (pull request #206)
IO-1350 Audatex import issues.
2021-09-09 20:10:20 +00:00
Patrick Fic
e46307e715 IO-1350 Audatex import issues. 2021-09-09 13:09:05 -07:00
Patrick Fic
f3e078b481 IO-233 Additional CDK Posting 2021-09-08 15:11:24 -07:00
Patrick Fic
c03f54b3eb Merge branch 'release/2021-09-10' into feature/cdk-cert 2021-09-08 08:28:54 -07:00
Patrick Fic
bb0234fb7d Merge branch 'test' into feature/cdk-cert 2021-09-08 08:28:27 -07:00
Patrick Fic
03f25c8c0e Merged in release/2021-09-10 (pull request #205)
release/2021-09-10

Approved-by: Patrick Fic
2021-09-07 23:53:40 +00:00
Patrick Fic
8e5005daa0 IO-1345 IO-43 Related Job linking 2021-09-07 16:27:34 -07:00
Patrick Fic
259458eec3 IO-1343 Updated tax rate check on import. 2021-09-07 09:53:55 -07:00
Patrick Fic
59e57aa274 Package updates. 2021-09-07 09:12:35 -07:00
Patrick Fic
de33bcd72b IO-1342 Revert IO-554 and add validation on jobline upsert. 2021-09-07 09:12:31 -07:00
Patrick Fic
d72472ccc3 Merged in hotifx/2021-09-06 (pull request #203)
hotifx/2021-09-06

Approved-by: Patrick Fic
2021-09-06 22:50:14 +00:00
Patrick Fic
91efd170c8 Merged in hotifx/2021-09-06 (pull request #202)
IO-1346 Resolve delivery checklist freezing.

Approved-by: Patrick Fic
2021-09-06 22:43:24 +00:00
Patrick Fic
6f1ddd51fd IO-1346 Resolve delivery checklist freezing. 2021-09-06 15:42:25 -07:00
Patrick Fic
9f48a91a29 Merged in release/2021-09-03 (pull request #201)
Remove assist tracker again.

Approved-by: Patrick Fic
2021-09-03 20:01:07 +00:00
Patrick Fic
a7e2548e14 Merged in release/2021-09-03 (pull request #200)
Remove assist tracker again.

Approved-by: Patrick Fic
2021-09-03 20:00:43 +00:00
Patrick Fic
3294faaeaa Remove assist tracker again. 2021-09-03 13:00:02 -07:00
Patrick Fic
06480159e3 Merge branch 'release/2021-09-03' into feature/qbo 2021-09-03 11:24:42 -07:00
Patrick Fic
6ce06ed5c0 Merge branch 'release/2021-09-03' into feature/cdk-cert 2021-09-03 11:24:16 -07:00
Patrick Fic
71e535388a Merged in release/2021-09-03 (pull request #199)
Release/2021 09 03
2021-09-03 18:19:02 +00:00
Patrick Fic
dbd265d368 Merged in release/2021-09-03 (pull request #198)
IO-1342 Resolve negative parts discount for negatives.

Approved-by: Patrick Fic
2021-09-02 23:31:38 +00:00
Patrick Fic
c11f182f83 IO-1342 Resolve negative parts discount for negatives. 2021-09-02 16:25:07 -07:00
Patrick Fic
5f70cfd585 Merged in release/2021-09-03 (pull request #197)
release/2021-09-03

Approved-by: Patrick Fic
2021-09-02 22:50:50 +00:00
Patrick Fic
6e0675f28b IO-1342 Resolve negative parts discount crash on job totals. 2021-09-02 15:48:54 -07:00
Patrick Fic
96f45d2c80 IO-233 Begin Trans Headers 2021-09-02 15:30:03 -07:00
Patrick Fic
091e44f471 Merge branch 'release/2021-09-03' into feature/cdk-cert 2021-09-02 15:27:22 -07:00
Patrick Fic
00549d6a88 Revert "Remove assist tracker."
This reverts commit d940e0ee78.
2021-09-01 18:10:44 -07:00
Patrick Fic
d940e0ee78 Remove assist tracker. 2021-09-01 15:06:52 -07:00
Patrick Fic
39a38d46ee Merged in release/2021-09-03 (pull request #196)
release/2021-09-03

Approved-by: Patrick Fic
2021-09-01 21:14:34 +00:00
Patrick Fic
71435ed75a Added patch for peerjs. 2021-09-01 14:14:20 -07:00
Patrick Fic
e7ec408b98 Merged in release/2021-09-03 (pull request #195)
Openrelay tracking change.

Approved-by: Patrick Fic
2021-09-01 20:39:45 +00:00
Patrick Fic
9306064420 Openrelay tracking change. 2021-09-01 13:38:35 -07:00
Patrick Fic
37a16edfb4 IO-233 Updated translations. 2021-09-01 13:24:19 -07:00
Patrick Fic
9ad5b9547f Merged in release/2021-09-03 (pull request #194)
release/2021-09-03

Approved-by: Patrick Fic
2021-09-01 16:19:58 +00:00
Patrick Fic
47edb0bdf4 IO-1332 Resolve checklist prod note. 2021-09-01 09:19:11 -07:00
Patrick Fic
5db47e879c IO-1224 Adjust scoreboard Entry 2021-09-01 08:43:37 -07:00
Patrick Fic
54b46dd25e IO-256 WIP QBO. 2021-09-01 08:31:24 -07:00
Patrick Fic
4cb92c8508 Merged in release/2021-09-03 (pull request #193)
IO-1336 Resolve towing/storage missing on export.

Approved-by: Patrick Fic
2021-08-31 00:04:22 +00:00
Patrick Fic
cb76e2dcde IO-1336 Resolve issues with QB Export of towing. 2021-08-30 16:59:15 -07:00
Patrick Fic
901c64ed85 IO-1336 Resolve towing/storage missing on export. 2021-08-30 16:38:45 -07:00
Patrick Fic
f6f90d68fa Merged in release/2021-09-03 (pull request #192)
release/2021-09-03

Approved-by: Patrick Fic
2021-08-30 22:43:40 +00:00
Patrick Fic
05e295fcac IO-1334 Added shop info RBAC. 2021-08-30 15:43:10 -07:00
Patrick Fic
c97df6dc61 IO-1333 Resolve dashboard component name issue. 2021-08-30 15:34:19 -07:00
Patrick Fic
61406aafa6 IO-1302 Remove 2nd message on job close. 2021-08-30 15:28:22 -07:00
Patrick Fic
03210db711 IO-1332 Resolve production note save on intake. 2021-08-30 15:25:04 -07:00
Patrick Fic
b3a34c109a IO-539 Fix hanging confirmation. 2021-08-30 15:20:08 -07:00
Patrick Fic
81daad35d8 Package updates and firebase refactor to SDK 9. 2021-08-30 15:09:11 -07:00
Patrick Fic
529eb24d76 Merge branch 'feature/qbo' into release/2021-09-03 2021-08-30 12:57:38 -07:00
Patrick Fic
e81bf7b561 Merge in CDK Branches WIP. 2021-08-30 12:57:13 -07:00
Patrick Fic
7a35dc9b38 IO-233 Add Vehicle History 2021-08-30 12:54:43 -07:00
Patrick Fic
c72ef97b82 IO-256 Further work on QBO Receivables. 2021-08-30 10:50:56 -07:00
Patrick Fic
5284ee2ef9 IO-256 Authorization and Basic Calls 2021-08-27 15:42:32 -07:00
Patrick Fic
724c097d52 IO-256 QBO Authorization Flow. 2021-08-26 15:48:10 -07:00
Patrick Fic
3c3da178ba Merged in release/2021-08-27 (pull request #191)
Release/2021 08 27
2021-08-25 21:59:23 +00:00
Patrick Fic
db4e5d48af Merge branch 'feature/cdk-cert' into feature/qbo 2021-08-25 12:08:06 -07:00
Patrick Fic
a7cf081ed5 IO-233 Begin Wip Header Creation 2021-08-24 18:48:02 -07:00
Patrick Fic
db5b11f6d3 IO-233 Vehicle and Customer posting unit testing completed. 2021-08-24 18:14:41 -07:00
Patrick Fic
8d3d52485f Merged in release/2021-08-27 (pull request #190)
IO-1330 Resolve postal code on qb export.

Approved-by: Patrick Fic
2021-08-24 23:40:02 +00:00
Patrick Fic
971b518e8f Merged in release/2021-08-27 (pull request #189)
IO-539 Resolve tax import for SGI.

Approved-by: Patrick Fic
2021-08-24 23:35:15 +00:00
Patrick Fic
748f8f472d IO-233 Insert DMS Vehicle 2021-08-24 15:56:10 -07:00
Patrick Fic
db2b4739c2 IO-233 Add insert Job. 2021-08-24 08:05:24 -07:00
Patrick Fic
6c12e5cb03 IO-233 Export Job Refactor 2021-08-23 19:02:05 -07:00
Patrick Fic
140e57a123 Merged in release/2021-08-27 (pull request #188)
IO-1330 Update QB Information

Approved-by: Patrick Fic
2021-08-23 21:17:06 +00:00
Patrick Fic
3ca6791939 IO-233 Added vehicle search & selection on form. 2021-08-23 10:40:10 -07:00
Patrick Fic
7e145bdec7 Merged in release/2021-08-27 (pull request #187)
Remove tracker assist for CI.

Approved-by: Patrick Fic
2021-08-23 15:43:39 +00:00
Patrick Fic
c493f6e31e Merged in release/2021-08-27 (pull request #186)
Release/2021 08 27

Approved-by: Patrick Fic
2021-08-23 15:34:50 +00:00
Patrick Fic
8bfd0a1c16 IO-233 Insert DMS Vehicles 2021-08-23 08:30:30 -07:00
Patrick Fic
834966ae96 Merged in release/2021-08-20 (pull request #185)
Release/2021 08 20
2021-08-20 21:01:17 +00:00
Patrick Fic
e7d813c3f3 Merged in test (pull request #184)
Merge WIP items back into CDK CErt.
2021-08-20 16:37:44 +00:00
Patrick Fic
db64d9b69f IO-233 WIP CDK 2021-08-17 16:22:10 -07:00
Patrick Fic
d6fbf16272 Merged in feature/2021-08-13 (pull request #180)
Feature/2021 08 13
2021-08-13 19:09:00 +00:00
Patrick Fic
2f9d025fbe Merged in hotfix/2021-08-12 (pull request #178)
hotfix/2021-08-12

Approved-by: Patrick Fic
2021-08-12 20:43:16 +00:00
Patrick Fic
9c1ffaba17 Merged in hotfix/2021-08-12 (pull request #177)
hotfix/2021-08-12
2021-08-12 20:42:55 +00:00
101 changed files with 41770 additions and 49086 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

43861
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,85 +4,87 @@
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@apollo/client": "^3.3.21",
"@apollo/client": "^3.4.10",
"@craco/craco": "^6.2.0",
"@fingerprintjs/fingerprintjs": "^3.2.0",
"@fingerprintjs/fingerprintjs": "^3.3.0",
"@lourenci/react-kanban": "^2.1.0",
"@openreplay/tracker": "^3.2.1",
"@openreplay/tracker-assist": "^3.0.3",
"@openreplay/tracker": "^3.3.1",
"@openreplay/tracker-assist": "^3.1.1",
"@openreplay/tracker-graphql": "^3.0.0",
"@openreplay/tracker-redux": "^3.0.0",
"@sentry/react": "^6.10.0",
"@sentry/tracing": "^6.10.0",
"@sentry/react": "^6.11.0",
"@sentry/tracing": "^6.11.0",
"@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.16.0",
"@tanem/react-nprogress": "^3.0.74",
"antd": "^4.16.8",
"@stripe/stripe-js": "^1.17.1",
"@tanem/react-nprogress": "^3.0.79",
"antd": "^4.16.13",
"apollo-link-logger": "^2.0.0",
"axios": "^0.21.1",
"craco-less": "^1.18.0",
"axios": "^0.21.4",
"craco-less": "^1.20.0",
"dinero.js": "^1.9.0",
"dotenv": "^10.0.0",
"enquire-js": "^0.2.1",
"env-cmd": "^10.1.0",
"exifr": "^7.1.2",
"firebase": "^8.7.1",
"graphql": "^15.5.1",
"i18next": "^20.3.4",
"exifr": "^7.1.3",
"firebase": "^9.0.0",
"graphql": "^15.5.3",
"i18next": "^20.4.0",
"i18next-browser-languagedetector": "^6.1.2",
"jsoneditor": "^9.5.2",
"jsoneditor": "^9.5.4",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.22",
"logrocket": "^1.2.0",
"markerjs2": "^2.9.0",
"libphonenumber-js": "^1.9.26",
"logrocket": "^2.0.0",
"markerjs2": "^2.11.2",
"moment-business-days": "^1.2.0",
"phone": "^3.1.2",
"phone": "^3.1.6",
"preval.macro": "^5.0.0",
"prop-types": "^15.7.2",
"query-string": "^7.0.1",
"rc-queue-anim": "^2.0.0",
"rc-scroll-anim": "^2.7.6",
"react": "^17.0.1",
"react-big-calendar": "^0.33.2",
"react-big-calendar": "^0.35.0",
"react-color": "^2.19.3",
"react-cookie": "^4.1.1",
"react-dom": "^17.0.1",
"react-drag-listview": "^0.1.8",
"react-grid-gallery": "^0.5.5",
"react-grid-layout": "^1.2.5",
"react-i18next": "^11.11.3",
"react-grid-layout": "^1.3.0",
"react-i18next": "^11.11.4",
"react-icons": "^4.2.0",
"react-number-format": "^4.6.4",
"react-redux": "^7.2.4",
"react-number-format": "^4.7.3",
"react-redux": "^7.2.5",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.2.0",
"react-router-dom": "^5.3.0",
"react-scripts": "^4.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.0.10",
"redux": "^4.1.0",
"recharts": "^2.1.2",
"redux": "^4.1.1",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"reselect": "^4.0.0",
"sass": "^1.35.2",
"socket.io-client": "^4.1.3",
"styled-components": "^5.3.0",
"sass": "^1.38.2",
"socket.io-client": "^4.2.0",
"styled-components": "^5.3.1",
"subscriptions-transport-ws": "^0.9.18",
"web-vitals": "^2.1.0",
"workbox-background-sync": "^6.1.5",
"workbox-broadcast-update": "^6.1.5",
"workbox-cacheable-response": "^6.1.5",
"workbox-core": "^6.1.5",
"workbox-expiration": "^6.1.5",
"workbox-google-analytics": "^6.1.5",
"workbox-navigation-preload": "^6.1.5",
"workbox-precaching": "^6.1.5",
"workbox-range-requests": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-streams": "^6.1.5"
"workbox-background-sync": "^6.2.4",
"workbox-broadcast-update": "^6.2.4",
"workbox-cacheable-response": "^6.2.4",
"workbox-core": "^6.2.4",
"workbox-expiration": "^6.2.4",
"workbox-google-analytics": "^6.2.4",
"workbox-navigation-preload": "^6.2.4",
"workbox-precaching": "^6.2.4",
"workbox-range-requests": "^6.2.4",
"workbox-routing": "^6.2.4",
"workbox-strategies": "^6.2.4",
"workbox-streams": "^6.2.4"
},
"scripts": {
"postinstall": "patch-package",
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "craco start",
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
@@ -113,6 +115,7 @@
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.17.1",
"patch-package": "^6.4.7",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2"
}

File diff suppressed because one or more lines are too long

View File

@@ -15,19 +15,18 @@ import Tracker from "@openreplay/tracker";
moment.locale("en-US");
export const tracker = new Tracker({
projectKey: "FPjeYIbwJyvhrVVTTLHo",
ingestPoint: "https://replay.bodyshop.app/ingest",
// ...(process.env.NODE_ENV === null || process.env.NODE_ENV === "development"
// ? { __DISABLE_SECURE_MODE: true }
// : {}),
projectKey: "trDmOZlEXUpjGsMtHroA",
ingestPoint: "https://replay.imex.online/ingest",
...(process.env.NODE_ENV === null || process.env.NODE_ENV === "development"
? { __DISABLE_SECURE_MODE: true }
: {}),
// beaconSize: 10485760,
onStart: ({ sessionID }) =>
console.log(
"******** OpenReplay tracker started with session: ",
sessionID
),
onStart: ({ sessionID }) => console.log("ORS SESSION ", sessionID),
});
//tracker.use(trackerAssist({ confirmText: "Confimr hep" })); // check the list of available options below
// tracker.use(
// trackerAssist({ confirmText: "Technical support is about to assist you." })
// ); // check the list of available options below
export const recordGraphQL = tracker.use(trackerGraphQL());
tracker.start();
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");

View File

@@ -1,25 +1,9 @@
import Axios from "axios";
import React from "react";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
export default function Test() {
const handleQbSignIn = async () => {
const result = await Axios.post("/qbo/authorize", { userId: "1234" });
console.log("handleQbSignIn -> result", result.data);
// window.open(result.data, "_blank", "toolbar=0,location=0,menubar=0");
var parameters = "location=1,width=800,height=650";
parameters +=
",left=" +
(window.screen.width - 800) / 2 +
",top=" +
(window.screen.height - 650) / 2;
// Launch Popup
window.open(result.data, "connectPopup", parameters);
};
return (
<div>
<button onClick={handleQbSignIn}>Sign Into Qb.</button>
<QboAuthorizeComponent />
</div>
);
}

View File

@@ -8,7 +8,25 @@ import { alphaSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
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
)(AccountingReceivablesTableComponent);
export function AccountingReceivablesTableComponent({
bodyshop,
loading,
jobs,
}) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -180,12 +198,14 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
<Card
extra={
<Space wrap>
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
/>
{!bodyshop.cdk_dealerid && (
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
/>
)}
<Input.Search
value={state.search}
onChange={handleSearch}

View File

@@ -244,7 +244,7 @@ const componentList = {
h: 3,
},
MonthlyPartsSales: {
label: i18next.t("dashboard.titles.productiondollars"),
label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
@@ -253,7 +253,7 @@ const componentList = {
h: 2,
},
MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlypartssales"),
label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,

View File

@@ -1,25 +1,39 @@
import { Button, Table } from "antd";
import React, { useState } from "react";
import { Button, Card, Table, Typography } from "antd";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js";
import { SyncOutlined } from "@ant-design/icons";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DmsAllocationsSummary);
export function DmsAllocationsSummary({ socket, bodyshop, jobId }) {
export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
useEffect(() => {
if (socket.connected) {
socket.emit("cdk-calculate-allocations", jobId, (ack) => {
setAllocationsSummary(ack);
socket.allocationsSummary = ack;
});
}
}, [socket, socket.connected, jobId]);
const columns = [
{
title: t("jobs.fields.dms.center"),
@@ -62,8 +76,9 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId }) {
];
return (
<Table
title={() => (
<Card
title={title}
extra={
<Button
onClick={() => {
socket.emit("cdk-calculate-allocations", jobId, (ack) =>
@@ -71,13 +86,50 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId }) {
);
}}
>
Get
<SyncOutlined />
</Button>
)}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
rowKey="center"
dataSource={allocationsSummary}
/>
}
>
<Table
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
rowKey="center"
dataSource={allocationsSummary}
summary={() => {
const totals = allocationsSummary.reduce(
(acc, val) => {
return {
totalSale: acc.totalSale.add(Dinero(val.sale)),
totalCost: acc.totalCost.add(Dinero(val.cost)),
};
},
{
totalSale: Dinero(),
totalCost: Dinero(),
}
);
return (
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{totals.totalSale.toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{
// totals.totalCost.toFormat()
}
</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>
);
}}
/>
</Card>
);
}

View File

@@ -1,9 +1,12 @@
import { useLazyQuery } from "@apollo/client";
import { Button, Input, Modal, Table } from "antd";
import React, { useState } from "react";
import { Modal, Button, Table, Input } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { SEARCH_DMS_VEHICLES } from "../../graphql/dms.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -12,83 +15,71 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakes);
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles);
export function DmsCdkMakes({ bodyshop, form, socket }) {
const [makesList, setMakesList] = useState([]);
const [searchText, setSearchText] = useState("");
const [loading, setLoading] = useState(false);
export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
const [visible, setVisible] = useState(false);
const [selectedModel, setSelectedModel] = useState(null);
const { t } = useTranslation();
const [callSearch, { loading, error, data }] =
useLazyQuery(SEARCH_DMS_VEHICLES);
const columns = [
{
title: t("jobs.fields.dms.makeFullName"),
dataIndex: "makeFullName",
key: "makeFullName",
title: t("vehicles.fields.v_make_desc"),
dataIndex: "make",
key: "make",
},
{
title: t("jobs.fields.dms.modelFullName"),
dataIndex: "modelFullName",
key: "modelFullName",
title: t("vehicles.fields.v_model_desc"),
dataIndex: "model",
key: "model",
},
{
title: t("jobs.fields.dms.makeCode"),
dataIndex: "makeCode",
key: "makeCode",
title: t("jobs.fields.dms.dms_make"),
dataIndex: "makecode",
key: "makecode",
},
{
title: t("jobs.fields.dms.modelCode"),
dataIndex: "modelCode",
key: "modelCode",
title: t("jobs.fields.dms.dms_model"),
dataIndex: "modelcode",
key: "modelcode",
},
];
const filteredMakes =
searchText !== "" && searchText
? makesList.filter(
(make) =>
searchText
.split(" ")
.some((v) =>
make.makeFullName.toLowerCase().includes(v.toLowerCase())
) ||
searchText
.split(" ")
.some((v) =>
make.modelFullName.toLowerCase().includes(v.toLowerCase())
)
)
: makesList;
return (
<div>
<Modal width={"90%"} visible={visible} onCancel={() => setVisible(false)}>
<Modal
width={"90%"}
visible={visible}
onCancel={() => setVisible(false)}
onOk={() => {
form.setFieldsValue({
dms_make: selectedModel.makecode,
dms_model: selectedModel.modelcode,
});
setVisible(false);
}}
>
{error && <AlertComponent error={error.message} />}
<Table
title={() => (
<Input.Search
onSearch={(val) => setSearchText(val)}
onSearch={(val) => callSearch({ variables: { search: val } })}
placeholder={t("general.labels.search")}
/>
)}
columns={columns}
loading={loading}
id="id"
dataSource={filteredMakes}
rowKey="id"
dataSource={data ? data.search_dms_vehicles : []}
onRow={(record) => {
return {
onClick: setSelectedModel(record),
onClick: () => setSelectedModel(record),
};
}}
rowSelection={{
onSelect: (record, selected, ...props) => {
console.log(
"🚀 ~ file: dms-cdk-makes.component.jsx ~ line 85 ~ record, selected, ...props",
record,
selected,
...props
);
onSelect: (record) => {
setSelectedModel(record);
},
@@ -100,15 +91,14 @@ export function DmsCdkMakes({ bodyshop, form, socket }) {
<Button
onClick={() => {
setVisible(true);
setLoading(true);
socket.emit("cdk-get-makes", bodyshop.cdk_dealerid, (makes) => {
console.log("Called back", makes);
setMakesList(makes);
setLoading(false);
callSearch({
variables: {
search: job && job.v_model_desc && job.v_model_desc.substr(0, 3),
},
});
}}
>
Get Makes
{t("jobs.actions.dms.findmakemodelcode")}
</Button>
</div>
);

View File

@@ -0,0 +1,34 @@
import { Button } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakesRefetch);
export function DmsCdkMakesRefetch({ bodyshop, form, socket }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const handleRefetch = async () => {
setLoading(true);
const response = await axios.post("/cdk/getvehicles", {
cdk_dealerid: bodyshop.cdk_dealerid,
bodyshopid: bodyshop.id,
});
console.log(response);
setLoading(false);
};
return (
<Button loading={loading} onClick={handleRefetch}>
{t("jobs.actions.dms.refetchmakesmodels")}
</Button>
);
}

View File

@@ -1,10 +1,24 @@
import { Button, Table } from "antd";
import { Button, Table, Col , Checkbox} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { socket } from "../../pages/dms/dms.container";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { alphaSort } from "../../utils/sorters";
export default function DmsCustomerSelector() {
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DmsCustomerSelector);
export function DmsCustomerSelector({ bodyshop }) {
const { t } = useTranslation();
const [customerList, setcustomerList] = useState([]);
const [visible, setVisible] = useState(false);
@@ -15,63 +29,93 @@ export default function DmsCustomerSelector() {
setcustomerList(customerList);
});
const onOk = () => {
const onUseSelected = () => {
setVisible(false);
socket.emit("cdk-selected-customer", selectedCustomer);
setSelectedCustomer(null);
};
const onUseGeneric = () => {
setVisible(false);
socket.emit(
"cdk-selected-customer",
bodyshop.cdk_configuration.generic_customer_number
);
setSelectedCustomer(null);
};
const onCreateNew = () => {
setVisible(false);
socket.emit("cdk-selected-customer", null);
setSelectedCustomer(null);
};
const columns = [
{
title: t("dms.fields.name1"),
title: t("jobs.fields.dms.id"),
dataIndex: ["id", "value"],
key: "id",
},
{
title: t("jobs.fields.dms.vinowner"),
dataIndex: "vinOwner",
key: "vinOwner",
render: (text, record) => <Checkbox disabled checked={record.vinOwner}/>
},
{
title: t("jobs.fields.dms.name1"),
dataIndex: ["name1", "fullName"],
key: "name1",
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName),
},
{
title: t("dms.fields.name2"),
dataIndex: ["name2", "fullName"],
key: "name2",
sorter: (a, b) => alphaSort(a.name2?.fullName, b.name2?.fullName),
},
{
title: t("dms.fields.phone"),
dataIndex: ["contactInfo", "mainTelephoneNumber", "value"],
key: "phone",
render: (record, value) => (
<PhoneFormatter>
{record.contactInfo?.mainTelephoneNumber?.value}
</PhoneFormatter>
),
},
{
title: t("dms.fields.address"),
title: t("jobs.fields.dms.address"),
//dataIndex: ["name2", "fullName"],
key: "address",
render: (record, value) =>
`${record.address?.addressLine[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`,
`${record?.address?.addressLine[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`,
},
];
if (!visible) return <></>;
return (
<Table
title={() => (
<div>
<Button onClick={onOk}>Select</Button>
</div>
)}
pagination={{ position: "top" }}
columns={columns}
rowKey={(record) => record.id.value}
dataSource={customerList}
//onChange={handleTableChange}
rowSelection={{
onSelect: (props) => {
setSelectedCustomer(props.id.value);
},
type: "radio",
selectedRowKeys: [selectedCustomer],
}}
/>
<Col span={24}>
<Table
title={() => (
<div>
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
{t("jobs.actions.dms.useselected")}
</Button>
<Button
onClick={onUseGeneric}
disabled={
!(
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.generic_customer_number
)
}
>
{t("jobs.actions.dms.usegeneric")}
</Button>
<Button onClick={onCreateNew}>
{t("jobs.actions.dms.createnewcustomer")}
</Button>
</div>
)}
pagination={{ position: "top" }}
columns={columns}
rowKey={(record) => record.id.value}
dataSource={customerList}
//onChange={handleTableChange}
rowSelection={{
onSelect: (props) => {
setSelectedCustomer(props.id.value);
},
type: "radio",
selectedRowKeys: [selectedCustomer],
}}
/>
</Col>
);
}

View File

@@ -1,13 +1,27 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input } from "antd";
import {
Button,
Card,
Divider,
Form,
Input,
InputNumber,
Select,
Space,
Statistic,
Typography,
} from "antd";
import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { determineDmsType } from "../../pages/dms/dms.container";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -17,134 +31,310 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
export function DmsPostForm({ bodyshop, socket, jobId }) {
export function DmsPostForm({ bodyshop, socket, job }) {
const [form] = Form.useForm();
const { t } = useTranslation();
const handlePayerSelect = (value, index) => {
form.setFieldsValue({
payers: form.getFieldValue("payers").map((payer, mapIndex) => {
if (index !== mapIndex) return payer;
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find((i) => i.name === value);
if (!cdkPayer) return payer;
return {
...cdkPayer,
dms_acctnumber: cdkPayer.dms_acctnumber,
controlnumber: job && job[cdkPayer.control_type],
};
}),
});
};
const handleFinish = (values) => {
socket.emit(`${determineDmsType(bodyshop)}-export-job`, {
jobid: job.id,
txEnvelope: values,
});
};
return (
<Form form={form} layout="vertical">
<LayoutFormRow>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<DmsCdkMakes form={form} socket={socket} />
</LayoutFormRow>
<Form.List name={["payers"]}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow>
<Form.Item
label={t("jobs.fields.dms.payer.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.account")}
key={`${index}account`}
name={[field.name, "account"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.controlnumber")}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
<Card title={t("jobs.labels.dms.postingform")}>
<Form
form={form}
layout="vertical"
onFinish={handleFinish}
initialValues={{
story: t("jobs.labels.dms.defaultstory", {
ro_number: job.ro_number,
area_of_damage: job.area_of_damage && job.area_of_damage.impact1,
}).substr(0, 239),
}}
</Form.List>
</Form>
>
<LayoutFormRow grow>
<Form.Item
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={
bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.default_journal
}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="kmin"
label={t("jobs.fields.kmin")}
initialValue={job && job.kmin}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled />
</Form.Item>
<Form.Item
name="kmout"
label={t("jobs.fields.kmout")}
initialValue={job && job.kmout}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber disabled />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow style={{ justifyContent: "center" }} grow>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
name="dms_model"
label={t("jobs.fields.dms.dms_model")}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<DmsCdkMakes form={form} socket={socket} job={job} />
<DmsCdkMakesRefetch />
</LayoutFormRow>
<Form.Item
name="story"
label={t("jobs.fields.dms.story")}
rules={[
{
required: true,
},
]}
>
<Input.TextArea maxLength={240} />
</Form.Item>
<Divider />
<Form.List name={["payers"]}>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space wrap>
<Form.Item
label={t("jobs.fields.dms.payer.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Select
style={{ minWidth: "15rem" }}
onSelect={(value) => handlePayerSelect(value, index)}
>
{bodyshop.cdk_configuration &&
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.map((payer) => (
<Select.Option key={payer.name}>
{payer.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0} />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.controlnumber")}
key={`${index}controlnumber`}
name={[field.name, "controlnumber"]}
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
const payers = form.getFieldValue("payers");
const row = payers && payers[index];
const cdkPayer =
bodyshop.cdk_configuration.payers &&
bodyshop.cdk_configuration.payers.find(
(i) => i && row && i.name === row.name
);
return (
<div>
{cdkPayer &&
t(`jobs.fields.${cdkPayer.control_type}`)}
</div>
);
}}
</Form.Item>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
disabled={!(fields.length < 3)}
onClick={() => {
if (fields.length < 3) add();
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({ amount: Math.round((payer?.amount || 0) * 100) })
);
});
const totals =
socket.allocationsSummary &&
socket.allocationsSummary.reduce(
(acc, val) => {
return {
totalSale: acc.totalSale.add(Dinero(val.sale)),
totalCost: acc.totalCost.add(Dinero(val.cost)),
};
},
{
totalSale: Dinero(),
totalCost: Dinero(),
}
);
const discrep = totals
? totals.totalSale.subtract(totalAllocated)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.subtotal")}
value={(totals ? totals.totalSale : Dinero()).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.dms.notallocated")}
valueStyle={{
color: discrep.getAmount() === 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
<Button
disabled={
!socket.allocationsSummary || discrep.getAmount() !== 0
}
htmlType="submit"
>
{t("jobs.actions.dms.post")}
</Button>
</Space>
);
}}
</Form.Item>
</Form>
</Card>
);
}

View File

@@ -2,7 +2,7 @@ import { withApollo } from "@apollo/client/react/hoc";
import React, { Component } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
//import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
@@ -15,21 +15,20 @@ const mapDispatchToProps = (dispatch) => ({
class FcmNotificationComponent extends Component {
async componentDidMount() {
//const { client, currentUser } = this.props;
if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
messaging
.requestPermission()
.then(async function () {
// const token = await messaging.getToken();
// client.mutate({
// mutation: UPDATE_FCM_TOKEN,
// variables: { authEmail: currentUser.email, token: { [token]: true } },
// });
})
.catch(function (err) {
console.log("Unable to get permission to notify.", err);
logImEXEvent("fcm_permission_denied", { message: err });
});
// if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
// messaging
// .requestPermission()
// .then(async function () {
// // const token = await messaging.getToken();
// // client.mutate({
// // mutation: UPDATE_FCM_TOKEN,
// // variables: { authEmail: currentUser.email, token: { [token]: true } },
// // });
// })
// .catch(function (err) {
// console.log("Unable to get permission to notify.", err);
// logImEXEvent("fcm_permission_denied", { message: err });
// });
}
render() {

View File

@@ -168,7 +168,6 @@ export default function GlobalSearch() {
<AutoComplete
options={options}
onSearch={handleSearch}
allowClear
placeholder={t("general.labels.globalsearch")}
>
<Input.Search loading={loading} />

View File

@@ -64,14 +64,16 @@ export function JobChecklistForm({
...(type === "intake" && { actual_in: new Date() }),
...(type === "intake" && {
production_vars: {
...job.production_vars,
...values.production_vars,
...(job ? job.production_vars : {}),
note:
values.production_vars &&
values.production_vars.note &&
values.production_vars.note !== ""
? job.production_vars && values.production_vars.note
: job.production_vars && job.production_vars.note,
? values &&
values.production_vars &&
values.production_vars.note
: job && job.production_vars && job.production_vars.note,
},
}),
...(type === "intake" && {
@@ -245,6 +247,7 @@ export function JobChecklistForm({
name={["production_vars", "note"]}
label={t("jobs.fields.production_vars.note")}
disabled={readOnly}
trigger="onChange"
>
<Input.TextArea rows={3} disabled={readOnly} />
</Form.Item>

View File

@@ -115,18 +115,18 @@ export default function JobLinesUpsertModalComponent({
<Form.Item
label={t("joblines.fields.mod_lb_hrs")}
name="mod_lb_hrs"
// rules={[
// ({ getFieldValue }) => ({
// validator(rule, value) {
// if (!!getFieldValue("mod_lbr_ty") === !!value) {
// return Promise.resolve();
// }
// return Promise.reject(
// t("joblines.validations.hrsrequirediflbrtyp")
// );
// },
// }),
// ]}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!!getFieldValue("mod_lbr_ty") === !!value) {
return Promise.resolve();
}
return Promise.reject(
t("joblines.validations.hrsrequirediflbrtyp")
);
},
}),
]}
>
<InputNumber precision={1} />
</Form.Item>
@@ -169,18 +169,18 @@ export default function JobLinesUpsertModalComponent({
<Form.Item
label={t("joblines.fields.part_qty")}
name="part_qty"
// rules={[
// ({ getFieldValue }) => ({
// validator(rule, value) {
// if (!!getFieldValue("part_type") === !!value) {
// return Promise.resolve();
// }
// return Promise.reject(
// t("joblines.validations.requiredifparttype")
// );
// },
// }),
// ]}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!!getFieldValue("part_type") === !!value) {
return Promise.resolve();
}
return Promise.reject(
t("joblines.validations.requiredifparttype")
);
},
}),
]}
>
<InputNumber precision={0} min={0} />
</Form.Item>
@@ -190,28 +190,28 @@ export default function JobLinesUpsertModalComponent({
<Form.Item
label={t("joblines.fields.act_price")}
name="act_price"
// rules={[
// ({ getFieldValue }) => ({
// validator(rule, value) {
// if (!value || getFieldValue("part_type") !== "PAE") {
// return Promise.resolve();
// }
// return Promise.reject(
// t("joblines.validations.zeropriceexistingpart")
// );
// },
// }),
// ({ getFieldValue }) => ({
// validator(rule, value) {
// if (!!getFieldValue("part_type") === !!value) {
// return Promise.resolve();
// }
// return Promise.reject(
// t("joblines.validations.requiredifparttype")
// );
// },
// }),
// ]}
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!value || getFieldValue("part_type") !== "PAE") {
return Promise.resolve();
}
return Promise.reject(
t("joblines.validations.zeropriceexistingpart")
);
},
}),
({ getFieldValue }) => ({
validator(rule, value) {
if (!!getFieldValue("part_type") === !!value) {
return Promise.resolve();
}
return Promise.reject(
t("joblines.validations.requiredifparttype")
);
},
}),
]}
>
<InputCurrency precision={2} min={0} />
</Form.Item>

View File

@@ -1,12 +1,11 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Form, notification, Popover } from "antd";
import { Button, Card, Form, InputNumber, notification, Popover } from "antd";
import moment from "moment";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
export default function ScoreboardAddButton({
job,
@@ -73,7 +72,7 @@ export default function ScoreboardAddButton({
},
]}
>
<InputNumberCalculator precision={1} />
<InputNumber precision={1} />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.painthrs")}
@@ -85,7 +84,7 @@ export default function ScoreboardAddButton({
},
]}
>
<InputNumberCalculator precision={1} />
<InputNumber precision={1} />
</Form.Item>
<Button type="primary" htmlType="submit">

View File

@@ -98,23 +98,7 @@ export function JobsAvailableContainer({
return;
}
//IO-539 Check for Parts Rate on PAL for SGI use case.
if (
estData.est_data.parts_tax_rates &&
estData.est_data.parts_tax_rates.PAL &&
(estData.est_data.parts_tax_rates.PAL.prt_tax_rt === null ||
estData.est_data.parts_tax_rates.PAL.prt_tax_rt === 0)
) {
console.log("checking");
const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for used parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}%.`
);
if (res) {
estData.est_data.parts_tax_rates.PAL.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAL.prt_tax_in = true;
}
}
await CheckTaxRates(estData, bodyshop);
const newTotals = (
await Axios.post("/job/totals", {
@@ -215,22 +199,7 @@ export function JobsAvailableContainer({
});
} else {
//IO-539 Check for Parts Rate on PAL for SGI use case.
if (
estData.est_data.parts_tax_rates &&
estData.est_data.parts_tax_rates.PAL &&
(estData.est_data.parts_tax_rates.PAL.prt_tax_rt === null ||
estData.est_data.parts_tax_rates.PAL.prt_tax_rt === 0)
) {
console.log("checking");
const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for used parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}%.`
);
if (res) {
estData.est_data.parts_tax_rates.PAL.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAL.prt_tax_in = true;
}
}
await CheckTaxRates(estData, bodyshop);
//create upsert job
let supp = replaceEmpty({ ...estData.est_data });
@@ -429,6 +398,101 @@ function confirmDialog(msg) {
return new Promise(function (resolve, reject) {
let confirmed = window.confirm(msg);
return confirmed ? resolve(true) : reject(false);
return confirmed ? resolve(true) : resolve(false);
});
}
async function CheckTaxRates(estData, bodyshop) {
//LKQ Check
if (
!estData.est_data.parts_tax_rates?.PAL ||
estData.est_data.parts_tax_rates?.PAL?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAL?.prt_tax_rt === 0
) {
const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for LKQ parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
);
if (res) {
if (!estData.est_data.parts_tax_rates.PAL) {
estData.est_data.parts_tax_rates.PAL = {
prt_discp: 0,
prt_mktyp: true,
prt_mkupp: 0,
prt_type: "PAL",
};
}
estData.est_data.parts_tax_rates.PAL.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAL.prt_tax_in = true;
}
}
//PAC Check
if (
!estData.est_data.parts_tax_rates?.PAC ||
estData.est_data.parts_tax_rates?.PAC?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAC?.prt_tax_rt === 0
) {
const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for rechromed parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
);
if (res) {
if (!estData.est_data.parts_tax_rates.PAC) {
estData.est_data.parts_tax_rates.PAC = {
prt_discp: 0,
prt_mktyp: true,
prt_mkupp: 0,
prt_type: "PAC",
};
}
estData.est_data.parts_tax_rates.PAC.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAC.prt_tax_in = true;
}
}
//PAM Check
if (
!estData.est_data.parts_tax_rates?.PAM ||
estData.est_data.parts_tax_rates?.PAM?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAM?.prt_tax_rt === 0
) {
const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for remanufactured parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
);
if (res) {
if (!estData.est_data.parts_tax_rates.PAM) {
estData.est_data.parts_tax_rates.PAM = {
prt_discp: 0,
prt_mktyp: true,
prt_mkupp: 0,
prt_type: "PAM",
};
}
estData.est_data.parts_tax_rates.PAM.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAM.prt_tax_in = true;
}
}
if (
!estData.est_data.parts_tax_rates?.PAR ||
estData.est_data.parts_tax_rates?.PAR?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAR?.prt_tax_rt === 0
) {
const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for recored parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
);
if (res) {
if (!estData.est_data.parts_tax_rates.PAR) {
estData.est_data.parts_tax_rates.PAR = {
prt_discp: 0,
prt_mktyp: true,
prt_mkupp: 0,
prt_type: "PAR",
};
}
estData.est_data.parts_tax_rates.PAR.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAR.prt_tax_in = true;
}
}
}

View File

@@ -13,6 +13,7 @@ import {
} from "../../redux/user/user.selectors";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -26,11 +27,17 @@ export function JobsCloseExportButton({
disabled,
setSelectedJobs,
}) {
const history = useHistory();
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [loading, setLoading] = useState(false);
const handleQbxml = async () => {
if (bodyshop.cdk_dealerid) {
history.push(`/manage/dms?jobId=${jobId}`);
return;
}
logImEXEvent("jobs_close_export");
setLoading(true);
@@ -159,12 +166,7 @@ export function JobsCloseExportButton({
};
return (
<Button
onClick={handleQbxml}
loading={loading}
disabled={disabled}
type="dashed"
>
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
{t("jobs.actions.export")}
</Button>
);

View File

@@ -15,6 +15,7 @@ import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import "./jobs-detail-header.styles.scss";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -80,6 +81,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<span style={{ margin: "0rem .5rem" }}>/</span>
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{job.alt_transport}
<JobAltTransportChange job={job} />
@@ -177,6 +179,9 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
{`${job.v_vin || t("general.labels.na")}`}
</DataLabel>
<DataLabel label={t("jobs.labels.relatedros")}>
<JobsRelatedRos jobid={job.id} job={job} />
</DataLabel>
</div>
</Card>
</Col>

View File

@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
@@ -22,9 +23,10 @@ import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
export function JobsDetailRates({ jobRO, form, job }) {
export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
const { t } = useTranslation();
return (
<div>
@@ -77,7 +79,7 @@ export function JobsDetailRates({ jobRO, form, job }) {
label={t("jobs.fields.adjustment_bottom_line")}
name="adjustment_bottom_line"
>
<CurrencyInput disabled={jobRO} />
<CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} />
</Form.Item>
<Space align="end">
<Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt">

View File

@@ -0,0 +1,20 @@
import { Space, Tag } from "antd";
import React from "react";
import { Link } from "react-router-dom";
export default function JobsRelatedRos({ jobid, job }) {
if (!(job && job.vehicle && job.vehicle.jobs)) return null;
return (
<Space wrap>
{job.vehicle.jobs
.filter((j) => j.id !== job.id)
.map((j) => (
<Tag key={j.id}>
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
j.clm_no ? ` | ${j.clm_no}` : ""
}${j.status ? ` | ${j.status}` : ""}`}</Link>
</Tag>
))}
</Space>
);
}

View File

@@ -57,6 +57,7 @@ export function LaborAllocationsTable({
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder:
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
render: (text, record) => `${record.cost_center} (${record.mod_lbr_ty})`,
},
{
title: t("jobs.labels.hrs_total"),

View File

@@ -16,6 +16,7 @@ export const CalculateAllocationsTotals = (
const r = {
opcode: value,
cost_center: responsibilitycenters.defaults.costs[value],
mod_lbr_ty: value,
total: joblines.reduce((acc2, val2) => {
return val2.mod_lbr_ty === value ? acc2 + val2.mod_lb_hrs : acc2;
}, 0),

View File

@@ -6,7 +6,6 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { auth } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { UPDATE_PAYMENTS } from "../../graphql/payments.queries";
import {
@@ -37,15 +36,9 @@ export function PaymentsExportAllButton({
let QbXmlResponse;
try {
QbXmlResponse = await axios.post(
"/accounting/qbxml/payments",
{ payments: paymentIds },
{
headers: {
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
},
}
);
QbXmlResponse = await axios.post("/accounting/qbxml/payments", {
payments: paymentIds,
});
} catch (error) {
console.log("Error getting QBXML from Server.", error);
notification["error"]({

View File

@@ -0,0 +1,76 @@
import { Button, Space } from "antd";
import Axios from "axios";
import React, { useEffect } from "react";
//import QboImg from "./qbo_signin.png";
import queryString from "query-string";
import { useLocation } from "react-router-dom";
import { useCookies } from "react-cookie";
export default function QboAuthorizeComponent() {
const location = useLocation();
const [, setCookie] = useCookies(["access_token", "refresh_token"]);
const handleQbSignIn = async () => {
const result = await Axios.post("/qbo/authorize");
console.log("pushing to history", result.data);
window.location.href = result.data;
};
const qs = queryString.parse(location.search);
const { error } = qs;
useEffect(() => {
const { code, state, realmId } = qs;
const hasBeenCalledBack = code && realmId && state;
if (hasBeenCalledBack) {
setCookie("qbo_code", code, { path: "/" });
setCookie("qbo_state", state, { path: "/" });
let expires = new Date();
expires.setTime(expires.getTime() + 8726400 * 1000);
setCookie("qbo_realmId", realmId, {
path: "/",
expires,
});
}
}, [qs, location, setCookie]);
return (
<div>
<Space wrap>
<Button onClick={handleQbSignIn}>
{/* <img
src={QboImg}
alt="Sign in with Intuit"
onClick={handleQbSignIn}
/> */}
auth
</Button>
<Button
onClick={async () => {
const response = await Axios.get(`/qbo/refresh`, {
withCredentials: true,
});
console.log(response);
}}
>
Refresh Token
</Button>
<Button
onClick={async () => {
const response = await Axios.post(`/qbo/receivables`, {
withCredentials: true,
});
console.log(response);
}}
>
REC
</Button>
</Space>
{error && JSON.parse(decodeURIComponent(error)).error_description}
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -52,8 +52,9 @@ const ret = {
"shiftclock:view": 2,
"shop:config": 4,
"shop:rbac": 5,
"shop:vendors": 2,
"shop:rbac": 1,
"shop:dashboard": 3,
"shop:templates": 4,

View File

@@ -1,10 +1,9 @@
import { Button, Card, Dropdown, Form, notification } from "antd";
import React, { useState } from "react";
import { useMutation } from "@apollo/client";
import { Button, Card, Dropdown, Form, InputNumber, notification } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
export default function ScoreboardEntryEdit({ entry }) {
const [visible, setVisible] = useState(false);
@@ -64,7 +63,7 @@ export default function ScoreboardEntryEdit({ entry }) {
},
]}
>
<InputNumberCalculator precision={1} />
<InputNumber precision={1} />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.painthrs")}
@@ -76,7 +75,7 @@ export default function ScoreboardEntryEdit({ entry }) {
},
]}
>
<InputNumberCalculator precision={1} />
<InputNumber precision={1} />
</Form.Item>
<Button type="primary" loading={loading} htmlType="submit">

View File

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

View File

@@ -7,6 +7,7 @@ import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import ImEXOnlineLogo from "../../assets/logo192.png";
import { auth } from "../../firebase/firebase.utils";
import { checkActionCode } from "@firebase/auth";
import { validatePasswordResetStart } from "../../redux/user/user.actions";
import { selectPasswordReset } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
@@ -35,7 +36,7 @@ export function UserValidatePwReset({
useEffect(() => {
async function checkCodeValid() {
try {
const codeValid = await auth.checkActionCode(oobCode);
const codeValid = await checkActionCode(auth, oobCode);
console.log("codeValid :>> ", codeValid);
setCodeValid({ loading: false, ...codeValid });
} catch (error) {

View File

@@ -1,20 +1,18 @@
import "firebase/analytics";
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/database";
import "firebase/firestore";
import "firebase/messaging";
import { getAnalytics, logEvent } from "firebase/analytics";
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
//import { getMessaging } from "firebase/messaging";
import { store } from "../redux/store";
const config = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
firebase.initializeApp(config);
initializeApp(config);
export const auth = firebase.auth();
export const firestore = firebase.firestore();
export const analytics = firebase.analytics();
export default firebase;
export const auth = getAuth();
export const firestore = getFirestore();
export const analytics = getAnalytics();
//export default firebase;
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged((userAuth) => {
@@ -53,17 +51,17 @@ export const updateCurrentPassword = async (password) => {
// });
};
let messaging;
try {
messaging = firebase.messaging();
// Project Settings => Cloud Messaging => Web Push certificates
messaging.usePublicVapidKey(process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY);
console.log("[FCM UTIL] FCM initialized successfully.");
} catch {
console.log("[FCM UTIL] Firebase Messaging is likely unsupported.");
}
//let messaging;
// try {
// messaging = getMessaging();
// // Project Settings => Cloud Messaging => Web Push certificates
// messaging.usePublicVapidKey(process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY);
// console.log("[FCM UTIL] FCM initialized successfully.");
// } catch {
// console.log("[FCM UTIL] Firebase Messaging is likely unsupported.");
// }
export { messaging };
// export { messaging };
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState();
@@ -82,59 +80,59 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
eventName,
eventParams
);
analytics.logEvent(eventName, eventParams);
logEvent(analytics, eventName, eventParams);
};
if (messaging) {
messaging.onMessage(async (payload) => {
console.log("[FCM] UTILS Message received. ", payload);
navigator.serviceWorker.getRegistration().then((registration) => {
return registration.showNotification(
"[UTIL]" + payload.notification.title,
payload.notification
);
});
// if (messaging) {
// onMessage(async (payload) => {
// console.log("[FCM] UTILS Message received. ", payload);
// navigator.serviceWorker.getRegistration().then((registration) => {
// return registration.showNotification(
// "[UTIL]" + payload.notification.title,
// payload.notification
// );
// });
// if (!payload.clientId) return;
// // if (!payload.clientId) return;
// // Get the client.
// const client = await clients.get(payload.clientId);
// // Exit early if we don't get the client.
// // Eg, if it closed.
// if (!client) return;
// // // Get the client.
// // const client = await clients.get(payload.clientId);
// // // Exit early if we don't get the client.
// // // Eg, if it closed.
// // if (!client) return;
// // Send a message to the client.
// console.log("Posting to client.");
// client.postMessage({
// msg: "Hey I just got a fetch from you!",
// url: payload.request.url,
// });
// // // Send a message to the client.
// // console.log("Posting to client.");
// // client.postMessage({
// // msg: "Hey I just got a fetch from you!",
// // url: payload.request.url,
// // });
// [START_EXCLUDE]
// Update the UI to include the received message.
//appendMessage(payload);
// // [START_EXCLUDE]
// // Update the UI to include the received message.
// //appendMessage(payload);
// [END_EXCLUDE]
});
// // [END_EXCLUDE]
// });
messaging.onTokenRefresh(() => {
messaging
.getToken()
.then((refreshedToken) => {
console.log("[FCM] Token refreshed.");
// Indicate that the new Instance ID token has not yet been sent to the
// app server.
// setTokenSentToServer(false);
// // Send Instance ID token to app server.
// sendTokenToServer(refreshedToken);
// // [START_EXCLUDE]
// // Display new Instance ID token and clear UI of all previous messages.
// resetUI();
// [END_EXCLUDE]
})
.catch((err) => {
console.log("[FCM] Unable to retrieve refreshed token ", err);
// showToken("Unable to retrieve refreshed token ", err);
});
});
}
// messaging.onTokenRefresh(() => {
// messaging
// .getToken()
// .then((refreshedToken) => {
// console.log("[FCM] Token refreshed.");
// // Indicate that the new Instance ID token has not yet been sent to the
// // app server.
// // setTokenSentToServer(false);
// // // Send Instance ID token to app server.
// // sendTokenToServer(refreshedToken);
// // // [START_EXCLUDE]
// // // Display new Instance ID token and clear UI of all previous messages.
// // resetUI();
// // [END_EXCLUDE]
// })
// .catch((err) => {
// console.log("[FCM] Unable to retrieve refreshed token ", err);
// // showToken("Unable to retrieve refreshed token ", err);
// });
// });
// }

View File

@@ -0,0 +1,13 @@
import { gql } from "@apollo/client";
export const SEARCH_DMS_VEHICLES = gql`
query SEARCH_DMS_VEHICLES($search: String) {
search_dms_vehicles(args: { search: $search }) {
id
make
makecode
model
modelcode
}
}
`;

View File

@@ -384,6 +384,12 @@ export const GET_JOB_BY_PK = gql`
v_model_desc
v_make_desc
v_color
jobs {
id
ro_number
status
clm_no
}
}
available_jobs {
id
@@ -700,6 +706,11 @@ export const QUERY_JOB_CARD_DETAILS = gql`
v_model_desc
v_color
plate_no
jobs {
id
clm_no
ro_number
}
}
actual_completion
actual_delivery
@@ -1726,6 +1737,8 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
actual_delivery
scheduled_in
actual_in
kmin
kmout
joblines(where: { removed: { _eq: false } }) {
id
removed
@@ -1878,98 +1891,67 @@ export const FIND_JOBS_BY_CLAIM = gql`
`;
export const QUERY_JOB_EXPORT_DMS = gql`
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
query QUERY_JOB_EXPORT_DMS($id: uuid!) {
jobs_by_pk(id: $id) {
ro_number
invoice_allocation
ins_co_id
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
ro_number
po_number
clm_no
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
status
date_exported
date_invoiced
voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
}
ownr_fn
ownr_ln
ownr_co_nm
kmin
kmout
v_make_desc
v_model_yr
v_model_desc
area_of_damage
}
}
`;
export const QUERY_RELATED_ROS = gql`
query QUERY_RELATED_ROS($jobid: uuid!) {
relatedjobs(
where: {
_or: [{ childjob: { _eq: $jobid } }, { parentjob: { _eq: $jobid } }]
}
joblines(where: { removed: { _eq: false } }) {
) {
parentjob
id
parentjob_rel {
id
removed
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
profitcenter_labor
profitcenter_part
prt_dsmk_p
ro_number
}
childjob
childjob_rel {
id
ro_number
}
}
}
`;
export const INSERT_RELATED_ROS = gql`
mutation INSERT_RELATED_ROS($relationship: relatedjobs_insert_input!) {
insert_relatedjobs_one(object: $relationship) {
parentjob
id
parentjob_rel {
id
ro_number
}
childjob
childjob_rel {
id
ro_number
}
}
}
`;
export const DELETE_RELATED_RO = gql`
mutation DELETE_RELATED_RO($relationshipid: uuid!) {
delete_relatedjobs_by_pk(id: $relationshipid) {
id
}
}
`;

View File

@@ -0,0 +1,47 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import QboAuthorizeComponent from "../../components/qbo-authorize/qbo-authorize.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export function AccountingReceivablesContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
}) {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.accounting-qbo");
setSelectedHeader("qbo");
setBreadcrumbs([
{
link: "/manage/accounting/qbo",
label: t("titles.bc.accounting-qbo"),
},
]);
}, [t, setBreadcrumbs, setSelectedHeader]);
return (
<div>
<QboAuthorizeComponent />
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AccountingReceivablesContainer);

View File

@@ -1,20 +1,29 @@
//import { useQuery } from "@apollo/client";
import { Button, Col, Result, Row, Select, Space } from "antd";
import { useQuery } from "@apollo/client";
import {
Button,
Card,
Col,
notification,
Result,
Row,
Select,
Space,
} from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { useLocation, useHistory } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
//import AlertComponent from "../../components/alert/alert.component";
import AlertComponent from "../../components/alert/alert.component";
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
//import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { auth } from "../../firebase/firebase.utils";
//import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import {
setBreadcrumbs,
setSelectedHeader,
@@ -47,20 +56,25 @@ export const socket = SocketIO(
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("TRACE");
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useHistory();
const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);
const { jobId } = search;
// const { loading, error } = useQuery(QUERY_JOB_EXPORT_DMS, {
// variables: { id: jobId },
// skip: true, //!jobId,
// });
const { loading, error, data } = useQuery(QUERY_JOB_EXPORT_DMS, {
variables: { id: jobId },
skip: !jobId,
});
useEffect(() => {
document.title = t("titles.dms");
setSelectedHeader("dms");
setBreadcrumbs([
{
link: "/manage/accounting/receivables",
label: t("titles.bc.accounting-receivables"),
},
{
link: "/manage/dms",
label: t("titles.bc.dms"),
@@ -69,11 +83,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
socket.on("connected", () => {
console.log("Connected again.");
});
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
console.log("Connected again.");
setLogs((logs) => {
return [
...logs,
@@ -91,10 +102,14 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
return [...logs, payload];
});
});
socket.on("export-success", (payload) => {
notification.success({
message: t("jobs.successes.exported"),
});
history.push("/manage/accounting/receivables");
});
socket.connect();
socket.emit("set-log-level", logLevel);
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
@@ -102,60 +117,79 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!jobId || !bodyshop.cdk_dealerid) return <Result status="404" />;
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
const dmsType = determineDmsType(bodyshop);
// if (loading) return <LoadingSpinner />;
// if (error) return <AlertComponent message={error.message} type="error" />;
if (!jobId || !bodyshop.cdk_dealerid || !(data && data.jobs_by_pk))
return <Result status="404" />;
return (
<div>
<Space>
<Button
onClick={() => {
socket.emit(
`${dmsType}-export-job`,
"752a4f5f-22ab-414b-b182-98d4e62227ef"
);
}}
>
Export
</Button>
<Select
placeholder="Log Level"
value={logLevel}
onChange={(value) => {
setLogLevel(value);
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
</Space>
<Row gutter={32}>
<Col span={18}>
<DmsAllocationsSummary socket={socket} jobId={jobId} />
<DmsPostForm socket={socket} jobId={jobId} />
<Row gutter={[16, 16]}>
<Col span={10}>
<DmsAllocationsSummary
title={`${data && data.jobs_by_pk && data.jobs_by_pk.ro_number} | ${
data.jobs_by_pk.ownr_fn || ""
} ${data.jobs_by_pk.ownr_ln || ""} ${
data.jobs_by_pk.ownr_co_nm || ""
} | ${data.jobs_by_pk.v_model_yr || ""} ${
data.jobs_by_pk.v_make_desc || ""
} ${data.jobs_by_pk.v_model_desc || ""}`}
socket={socket}
jobId={jobId}
/>
</Col>
<Col span={6}>
<div style={{ maxHeight: "500px", overflowY: "auto" }}>
<Col span={14}>
<DmsPostForm
socket={socket}
jobId={jobId}
job={data && data.jobs_by_pk}
/>
</Col>
<DmsCustomerSelector />
<Col span={24}>
<Card
title={t("jobs.labels.dms.logs")}
extra={
<Space wrap>
<Select
placeholder="Log Level"
value={logLevel}
onChange={(value) => {
setLogLevel(value);
socket.emit("set-log-level", value);
}}
>
<Select.Option key="TRACE">TRACE</Select.Option>
<Select.Option key="DEBUG">DEBUG</Select.Option>
<Select.Option key="INFO">INFO</Select.Option>
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button
onClick={() => {
setLogs([]);
socket.disconnect();
socket.connect();
}}
>
Reconnect
</Button>
</Space>
}
>
<DmsLogEvents socket={socket} logs={logs} />
</div>
</Card>
</Col>
</Row>
<DmsCustomerSelector />
</div>
);
}
const determineDmsType = (bodyshop) => {
export const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk";
else {
return "pbs";

View File

@@ -8,6 +8,7 @@ import {
Alert,
Divider,
PageHeader,
InputNumber,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -56,6 +57,8 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
actual_in: values.actual_in,
actual_completion: values.actual_completion,
actual_delivery: values.actual_delivery,
kmin: values.kmin,
kmout: values.kmout,
},
},
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
@@ -63,7 +66,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
});
if (!result.errors) {
notification["success"]({ message: t("jobs.successes.save") });
// notification["success"]({ message: t("jobs.successes.save") });
// form.resetFields();
} else {
notification["error"]({
@@ -112,6 +115,8 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
actual_delivery: job.actual_delivery
? moment(job.actual_delivery)
: job.scheduled_delivery && moment(job.scheduled_delivery),
kmin: job.kmin,
kmout: job.kmout,
}}
scrollToFirstError
>
@@ -203,6 +208,45 @@ export function JobsCloseComponent({ job, bodyshop, jobRO }) {
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("jobs.fields.kmin")}
name="kmin"
rules={[
{
required: true,
},
]}
>
<InputNumber precision={0} disabled={jobRO} />
</Form.Item>
)}
{bodyshop.cdk_dealerid && (
<Form.Item
label={t("jobs.fields.kmout")}
name="kmout"
dependencies={["kmin"]}
hasFeedback
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue("kmin") <= value) {
return Promise.resolve();
}
return Promise.reject(
new Error(t("jobs.labels.dms.kmoutnotgreaterthankmin"))
);
},
}),
]}
>
<InputNumber precision={0} disabled={jobRO} />
</Form.Item>
)}
</LayoutFormRow>
<Divider />
<JobsCloseLines job={job} />

View File

@@ -116,6 +116,9 @@ const JobChecklistView = lazy(() =>
const JobDeliver = lazy(() =>
import("../jobs-deliver/jobs-delivery.page.container")
);
const AccountingQboCallback = lazy(() =>
import("../accounting-qbo/accounting-qbo.page")
);
const AccountingReceivables = lazy(() =>
import("../accounting-receivables/accounting-receivables.container")
);
@@ -333,6 +336,13 @@ export function Manage({ match, conflict, bodyshop }) {
path={`${match.path}/shop/csi`}
component={ShopCsiPageContainer}
/>
<Route
exact
path={`${match.path}/accounting/qbo`}
component={AccountingQboCallback}
/>
<Route
exact
path={`${match.path}/accounting/receivables`}

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -35,20 +36,22 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
}, [t, setSelectedHeader, setBreadcrumbs, bodyshop.shopname]);
return (
<Tabs>
<Tabs.TabPane tab={t("bodyshop.labels.shopinfo")} key="info">
<ShopInfoContainer />
</Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.employees")} key="employees">
<ShopEmployeesContainer />
</Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.licensing")} key="licensing">
<ShopInfoUsersComponent />
</Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.csiq")} key="csiq">
<ShopCsiConfig />
</Tabs.TabPane>
</Tabs>
<RbacWrapper action="shop:config">
<Tabs>
<Tabs.TabPane tab={t("bodyshop.labels.shopinfo")} key="info">
<ShopInfoContainer />
</Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.employees")} key="employees">
<ShopEmployeesContainer />
</Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.licensing")} key="licensing">
<ShopInfoUsersComponent />
</Tabs.TabPane>
<Tabs.TabPane tab={t("bodyshop.labels.csiq")} key="csiq">
<ShopCsiConfig />
</Tabs.TabPane>
</Tabs>
</RbacWrapper>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ShopPage);

View File

@@ -1,12 +1,20 @@
import Fingerprint2 from "@fingerprintjs/fingerprintjs";
import * as Sentry from "@sentry/browser";
import { notification } from "antd";
import { auth, analytics, firestore } from "../../firebase/firebase.utils";
import { setUserId, setUserProperties } from "firebase/analytics";
import {
checkActionCode,
confirmPasswordReset,
signInWithEmailAndPassword,
signOut,
} from "firebase/auth";
import { doc } from "firebase/firestore";
import i18next from "i18next";
import LogRocket from "logrocket";
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
import { tracker } from "../../App/App.container";
import {
analytics,
auth,
firestore,
getCurrentUser,
logImEXEvent,
updateCurrentUser,
@@ -28,8 +36,6 @@ import {
validatePasswordResetSuccess,
} from "./user.actions";
import UserActionTypes from "./user.types";
import * as Sentry from "@sentry/browser";
import { tracker } from "../../App/App.container";
export function* onEmailSignInStart() {
yield takeLatest(UserActionTypes.EMAIL_SIGN_IN_START, signInWithEmail);
@@ -38,7 +44,7 @@ export function* signInWithEmail({ payload: { email, password } }) {
try {
logImEXEvent("redux_sign_in_attempt", { user: email });
const { user } = yield auth.signInWithEmailAndPassword(email, password);
const { user } = yield signInWithEmailAndPassword(auth, email, password);
yield put(
signInSuccess({
@@ -90,7 +96,7 @@ export function* signOutStart() {
try {
logImEXEvent("redux_sign_out");
yield auth.signOut();
yield signOut(auth);
yield put(signOutSuccess());
localStorage.removeItem("token");
} catch (error) {
@@ -104,10 +110,7 @@ export function* onUpdateUserDetails() {
export function* updateUserDetails(userDetails) {
try {
const updatedDetails = yield updateCurrentUser(userDetails.payload);
console.log(
"🚀 ~ file: user.sagas.js ~ line 104 ~ updatedDetails",
updatedDetails
);
yield put(updateUserDetailsSuccess(updatedDetails));
notification.open({
type: "success",
@@ -122,7 +125,7 @@ export function* onSetInstanceId() {
}
export function* setInstanceIdSaga({ payload: uid }) {
try {
const userInstanceRef = firestore.doc(`userInstance/${uid}`);
const userInstanceRef = doc(firestore, `userInstance/${uid}`);
const fingerprint = Fingerprint2.x64hash128(
(yield Fingerprint2.getPromise({})).map((c) => c.value).join(""),
@@ -147,7 +150,7 @@ export function* onCheckInstanceId() {
}
export function* checkInstanceIdSaga({ payload: uid }) {
try {
const userInstanceRef = firestore.doc(`userInstance/${uid}`);
const userInstanceRef = doc(firestore, `userInstance/${uid}`);
const snapshot = yield userInstanceRef.get();
let fingerprint = yield select((state) => state.user.fingerprint);
@@ -193,8 +196,8 @@ export function* signInSuccessSaga({ payload }) {
}
// if (!payload.email.includes("@imex.")) yield put(setInstanceId(payload.uid));
analytics.setUserId(payload.email);
analytics.setUserProperties(payload);
setUserId(analytics, payload.email);
setUserProperties(analytics, payload);
yield logImEXEvent("redux_sign_in_success");
}
@@ -210,7 +213,7 @@ export function* onSendPasswordResetStart() {
}
export function* sendPasswordResetEmail({ payload }) {
try {
yield auth.sendPasswordResetEmail(payload, {
yield sendPasswordResetEmail(payload, {
url: "https://imex.online/passwordreset",
});
@@ -228,8 +231,8 @@ export function* onValidatePasswordResetStart() {
}
export function* validatePasswordResetStart({ payload: { password, code } }) {
try {
auth.checkActionCode(code);
yield auth.confirmPasswordReset(code, password);
checkActionCode(auth, code);
yield confirmPasswordReset(auth, code, password);
yield put(validatePasswordResetSuccess());
} catch (error) {
console.log("function*validatePasswordResetStart -> error", error);

View File

@@ -232,10 +232,16 @@
"templates": "Delivery Templates"
},
"dms": {
"cashierid": "Cashier ID",
"default_journal": "Default Journal",
"dms_acctnumber": "DMS Account #",
"dms_wip_acctnumber": "DMS W.I.P. Account #",
"mappingname": "DMS Mapping Name"
"generic_customer_number": "Generic Customer Number",
"itc_federal": "Federal Tax is ITC?",
"itc_local": "Local Tax is ITC?",
"itc_state": "State Tax is ITC?",
"mappingname": "DMS Mapping Name",
"srcco": "Source Company #/Dealer #"
},
"email": "General Shop Email",
"enforce_class": "Enforce Class on Conversion?",
@@ -352,6 +358,7 @@
"view": "Shift Clock -> View"
},
"shop": {
"config": "Shop -> Config",
"dashboard": "Shop -> Dashboard",
"rbac": "Shop -> RBAC",
"templates": "Shop -> Templates",
@@ -381,6 +388,10 @@
"ar": "Accounts Receivable",
"ats": "ATS",
"federal_tax": "Federal Tax",
"la1": "LA1",
"la2": "LA2",
"la3": "LA3",
"la4": "LA4",
"lab": "Body",
"lad": "Diagnostic",
"lae": "Electrical",
@@ -402,6 +413,7 @@
"pap": "OEM Partial",
"par": "Recored",
"pas": "Sublet",
"pasl": "Sublet (L)",
"refund": "Refund",
"sales_tax_codes": {
"code": "Code",
@@ -474,6 +486,9 @@
"defaultprofitsmapping": "Default Profits Mapping",
"deliverchecklist": "Delivery Checklist",
"dms": {
"cdk": {
"payers": "CDK Payers"
},
"cdk_dealerid": "CDK Dealer ID",
"title": "DMS"
},
@@ -1026,7 +1041,7 @@
"PAP": "OEM Partial",
"PAR": "Recored",
"PAS": "Sublet",
"PASL": "Sublet"
"PASL": "Sublet (L)"
},
"profitcenter_labor": "Profit Center: Labor",
"profitcenter_part": "Profit Center: Part",
@@ -1066,6 +1081,16 @@
"changestatus": "Change Status",
"convert": "Convert",
"deliver": "Deliver",
"dms": {
"addpayer": "Add Payer",
"createnewcustomer": "Create New Customer",
"findmakemodelcode": "Find Make/Model Code",
"getmakes": "",
"post": "Post",
"refetchmakesmodels": "Refetch Make and Model Codes",
"usegeneric": "Use Generic Customer",
"useselected": "Use Selected Customer"
},
"dmsautoallocate": "DMS Auto Allocate",
"export": "Export",
"exportcustdata": "Export Customer Data",
@@ -1123,6 +1148,29 @@
"adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours",
"alt_transport": "Alt. Trans.",
"area_of_damage_impact": {
"10": "Left Front Side",
"11": "Left Front Corner",
"12": "Front",
"13": "Rollover",
"14": "Unknown",
"15": "Total Loss",
"16": "Non-collision",
"25": "Hood",
"26": "Deck-lid",
"27": "Roof",
"28": "Undercarriage",
"34": "All Over",
"01": "Right Front Corner",
"02": "Right Front Side",
"03": "Right Side",
"04": "Right Rear Side",
"05": "Right Rear Corner",
"06": "Rear",
"07": "Left Rear Corner",
"08": "Left Rear Side",
"09": "Left Side"
},
"ca_bc_pvrt": "PVRT",
"ca_customer_gst": "Customer Portion of GST",
"ca_gst_registrant": "GST Registrant",
@@ -1147,12 +1195,27 @@
"ded_status": "Deductible Status",
"depreciation_taxes": "Depreciation/Taxes",
"dms": {
"address": "Customer Address",
"center": "Center",
"cost": "Cost",
"cost_dms_acctnumber": "Cost DMS Acct #",
"dms_make": "DMS Make",
"dms_model": "DMS Model",
"dms_wip_acctnumber": "Cost WIP DMS Acct #",
"id": "DMS ID",
"journal": "Journal #",
"name1": "Customer Name",
"payer": {
"amount": "Amount",
"control_type": "Control Type",
"controlnumber": "Control Number",
"dms_acctnumber": "DMS Account #",
"name": "Payer Name"
},
"sale": "Sale",
"sale_dms_acctnumber": "Sale DMS Acct #"
"sale_dms_acctnumber": "Sale DMS Acct #",
"story": "Story",
"vinowner": "VIN Owner"
},
"driveable": "Driveable",
"employee_body": "Body",
@@ -1373,6 +1436,14 @@
"deliverchecklist": "Deliver Checklist",
"difference": "Difference",
"diskscan": "Scan Disk for Estimates",
"dms": {
"defaultstory": "Bodyshop RO {{ro_number}}. Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).",
"kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.",
"logs": "Logs",
"notallocated": "Not Allocated",
"postingform": "Posting Form",
"totalallocated": "Total Amount Allocated"
},
"documents": "Documents",
"documents-images": "Images",
"documents-other": "Other Documents",
@@ -1420,7 +1491,7 @@
"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."
},
"prt_dsmk_total": "Line Item Markup",
"prt_dsmk_total": "Line Item Adjustment",
"rates": "Rates",
"rates_subtotal": "All Rates Subtotal",
"reconciliation": {
@@ -1436,6 +1507,7 @@
"removedpartsstrikethrough": "Strike through lines represent parts that have been removed from the estimate. They are included for completeness of reconciliation."
},
"reconciliationheader": "Parts & Sublet Reconciliation",
"relatedros": "Related ROs",
"returntotals": "Return Totals",
"rosaletotal": "RO Parts Total",
"sale_labor": "Sales - Labor",
@@ -2213,6 +2285,7 @@
"courtesycars-detail": "Courtesy Car {{number}}",
"courtesycars-new": "New Courtesy Car",
"dashboard": "Dashboard",
"dms": "DMS Export",
"export-logs": "Export Logs",
"jobs": "Jobs",
"jobs-active": "Active Jobs",
@@ -2251,6 +2324,7 @@
"courtesycars-create": "New Courtesy Car | $t(titles.app)",
"courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)",
"dashboard": "Dashboard | $t(titles.app)",
"dms": "DMS Export | $t(titles.app)",
"export-logs": "Export Logs | $t(titles.app)",
"jobs": "Active Jobs | $t(titles.app)",
"jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)",

View File

@@ -232,10 +232,16 @@
"templates": ""
},
"dms": {
"cashierid": "",
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
"generic_customer_number": "",
"itc_federal": "",
"itc_local": "",
"itc_state": "",
"mappingname": "",
"srcco": ""
},
"email": "",
"enforce_class": "",
@@ -352,6 +358,7 @@
"view": ""
},
"shop": {
"config": "",
"dashboard": "",
"rbac": "",
"templates": "",
@@ -381,6 +388,10 @@
"ar": "",
"ats": "",
"federal_tax": "",
"la1": "",
"la2": "",
"la3": "",
"la4": "",
"lab": "",
"lad": "",
"lae": "",
@@ -402,6 +413,7 @@
"pap": "",
"par": "",
"pas": "",
"pasl": "",
"refund": "",
"sales_tax_codes": {
"code": "",
@@ -474,6 +486,9 @@
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk": {
"payers": ""
},
"cdk_dealerid": "",
"title": ""
},
@@ -1066,6 +1081,16 @@
"changestatus": "Cambiar Estado",
"convert": "Convertir",
"deliver": "",
"dms": {
"addpayer": "",
"createnewcustomer": "",
"findmakemodelcode": "",
"getmakes": "",
"post": "",
"refetchmakesmodels": "",
"usegeneric": "",
"useselected": ""
},
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
@@ -1123,6 +1148,29 @@
"adjustment_bottom_line": "Ajustes",
"adjustmenthours": "",
"alt_transport": "",
"area_of_damage_impact": {
"10": "",
"11": "",
"12": "",
"13": "",
"14": "",
"15": "",
"16": "",
"25": "",
"26": "",
"27": "",
"28": "",
"34": "",
"01": "",
"02": "",
"03": "",
"04": "",
"05": "",
"06": "",
"07": "",
"08": "",
"09": ""
},
"ca_bc_pvrt": "",
"ca_customer_gst": "",
"ca_gst_registrant": "",
@@ -1147,12 +1195,27 @@
"ded_status": "Estado deducible",
"depreciation_taxes": "Depreciación / Impuestos",
"dms": {
"address": "",
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
"dms_make": "",
"dms_model": "",
"dms_wip_acctnumber": "",
"id": "",
"journal": "",
"name1": "",
"payer": {
"amount": "",
"control_type": "",
"controlnumber": "",
"dms_acctnumber": "",
"name": ""
},
"sale": "",
"sale_dms_acctnumber": ""
"sale_dms_acctnumber": "",
"story": "",
"vinowner": ""
},
"driveable": "",
"employee_body": "",
@@ -1373,6 +1436,14 @@
"deliverchecklist": "",
"difference": "",
"diskscan": "",
"dms": {
"defaultstory": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
"notallocated": "",
"postingform": "",
"totalallocated": ""
},
"documents": "documentos",
"documents-images": "",
"documents-other": "",
@@ -1436,6 +1507,7 @@
"removedpartsstrikethrough": ""
},
"reconciliationheader": "",
"relatedros": "",
"returntotals": "",
"rosaletotal": "",
"sale_labor": "",
@@ -2213,6 +2285,7 @@
"courtesycars-detail": "",
"courtesycars-new": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "",
"jobs-active": "",
@@ -2251,6 +2324,7 @@
"courtesycars-create": "",
"courtesycars-detail": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "Todos los trabajos | $t(titles.app)",
"jobs-admin": "",

View File

@@ -232,10 +232,16 @@
"templates": ""
},
"dms": {
"cashierid": "",
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
"generic_customer_number": "",
"itc_federal": "",
"itc_local": "",
"itc_state": "",
"mappingname": "",
"srcco": ""
},
"email": "",
"enforce_class": "",
@@ -352,6 +358,7 @@
"view": ""
},
"shop": {
"config": "",
"dashboard": "",
"rbac": "",
"templates": "",
@@ -381,6 +388,10 @@
"ar": "",
"ats": "",
"federal_tax": "",
"la1": "",
"la2": "",
"la3": "",
"la4": "",
"lab": "",
"lad": "",
"lae": "",
@@ -402,6 +413,7 @@
"pap": "",
"par": "",
"pas": "",
"pasl": "",
"refund": "",
"sales_tax_codes": {
"code": "",
@@ -474,6 +486,9 @@
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk": {
"payers": ""
},
"cdk_dealerid": "",
"title": ""
},
@@ -1066,6 +1081,16 @@
"changestatus": "Changer le statut",
"convert": "Convertir",
"deliver": "",
"dms": {
"addpayer": "",
"createnewcustomer": "",
"findmakemodelcode": "",
"getmakes": "",
"post": "",
"refetchmakesmodels": "",
"usegeneric": "",
"useselected": ""
},
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
@@ -1123,6 +1148,29 @@
"adjustment_bottom_line": "Ajustements",
"adjustmenthours": "",
"alt_transport": "",
"area_of_damage_impact": {
"10": "",
"11": "",
"12": "",
"13": "",
"14": "",
"15": "",
"16": "",
"25": "",
"26": "",
"27": "",
"28": "",
"34": "",
"01": "",
"02": "",
"03": "",
"04": "",
"05": "",
"06": "",
"07": "",
"08": "",
"09": ""
},
"ca_bc_pvrt": "",
"ca_customer_gst": "",
"ca_gst_registrant": "",
@@ -1147,12 +1195,27 @@
"ded_status": "Statut de franchise",
"depreciation_taxes": "Amortissement / taxes",
"dms": {
"address": "",
"center": "",
"cost": "",
"cost_dms_acctnumber": "",
"dms_make": "",
"dms_model": "",
"dms_wip_acctnumber": "",
"id": "",
"journal": "",
"name1": "",
"payer": {
"amount": "",
"control_type": "",
"controlnumber": "",
"dms_acctnumber": "",
"name": ""
},
"sale": "",
"sale_dms_acctnumber": ""
"sale_dms_acctnumber": "",
"story": "",
"vinowner": ""
},
"driveable": "",
"employee_body": "",
@@ -1373,6 +1436,14 @@
"deliverchecklist": "",
"difference": "",
"diskscan": "",
"dms": {
"defaultstory": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
"notallocated": "",
"postingform": "",
"totalallocated": ""
},
"documents": "Les documents",
"documents-images": "",
"documents-other": "",
@@ -1436,6 +1507,7 @@
"removedpartsstrikethrough": ""
},
"reconciliationheader": "",
"relatedros": "",
"returntotals": "",
"rosaletotal": "",
"sale_labor": "",
@@ -2213,6 +2285,7 @@
"courtesycars-detail": "",
"courtesycars-new": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "",
"jobs-active": "",
@@ -2251,6 +2324,7 @@
"courtesycars-create": "",
"courtesycars-detail": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "Tous les emplois | $t(titles.app)",
"jobs-admin": "",

View File

@@ -14,6 +14,7 @@ export const axiosAuthInterceptorId = axios.interceptors.request.use(
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => Promise.reject(error)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: DROP TABLE "public"."dms_vehicles";
type: run_sql

View File

@@ -0,0 +1,18 @@
- args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
type: run_sql
- args:
cascade: false
read_only: false
sql: CREATE TABLE "public"."dms_vehicles"("id" uuid NOT NULL DEFAULT gen_random_uuid(),
"created_at" timestamptz NOT NULL DEFAULT now(), "makecode" text NOT NULL, "modelcode"
text NOT NULL, "make" text NOT NULL, "model" text NOT NULL, "bodyshopid" uuid
NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id")
ON UPDATE cascade ON DELETE cascade);
type: run_sql
- args:
name: dms_vehicles
schema: public
type: add_existing_table_or_view

View File

@@ -0,0 +1,12 @@
- args:
relationship: dms_vehicles
table:
name: bodyshops
schema: public
type: drop_relationship
- args:
relationship: bodyshop
table:
name: dms_vehicles
schema: public
type: drop_relationship

View File

@@ -0,0 +1,20 @@
- args:
name: dms_vehicles
table:
name: bodyshops
schema: public
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: dms_vehicles
schema: public
type: create_array_relationship
- args:
name: bodyshop
table:
name: dms_vehicles
schema: public
using:
foreign_key_constraint_on: bodyshopid
type: create_object_relationship

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: dms_vehicles
schema: public
type: drop_select_permission

View File

@@ -0,0 +1,28 @@
- args:
permission:
allow_aggregations: false
backend_only: false
columns:
- id
- created_at
- makecode
- modelcode
- make
- model
- bodyshopid
computed_fields: []
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
limit: null
role: user
table:
name: dms_vehicles
schema: public
type: create_select_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: dms_vehicles
schema: public
type: drop_insert_permission

View File

@@ -0,0 +1,27 @@
- args:
permission:
allow_upsert: true
backend_only: false
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- makecode
- modelcode
- make
- model
- bodyshopid
set: {}
role: user
table:
name: dms_vehicles
schema: public
type: create_insert_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: dms_vehicles
schema: public
type: drop_update_permission

View File

@@ -0,0 +1,26 @@
- args:
permission:
backend_only: false
columns:
- make
- makecode
- model
- modelcode
- created_at
- bodyshopid
- id
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
set: {}
role: user
table:
name: dms_vehicles
schema: public
type: create_update_permission

View File

@@ -0,0 +1,6 @@
- args:
role: user
table:
name: dms_vehicles
schema: public
type: drop_delete_permission

View File

@@ -0,0 +1,17 @@
- args:
permission:
backend_only: false
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: dms_vehicles
schema: public
type: create_delete_permission

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,13 @@
- args:
cascade: true
read_only: false
sql: CREATE OR REPLACE FUNCTION public.search_dms_vehicles(search text)RETURNS
SETOF dms_vehicles LANGUAGE plpgsql STABLE AS $FUNCTION$ BEGIN IF search=''
THEN RETURN query SELECT*FROM dms_vehicles;ELSE RETURN query SELECT*FROM dms_vehicles
WHERE make ILIKE'%'||search||'%' OR model ILIKE'%'||search||'%' ORDER BY make
ILIKE'%'||search||'%' OR NULL,model ILIKE'%'||search||'%' OR NULL;END IF;END$FUNCTION$;
type: run_sql
- args:
name: search_dms_vehicles
schema: public
type: track_function

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."associations" DROP COLUMN "qbo_auth";
type: run_sql

View File

@@ -0,0 +1,5 @@
- args:
cascade: false
read_only: false
sql: ALTER TABLE "public"."associations" ADD COLUMN "qbo_auth" jsonb NULL;
type: run_sql

View File

@@ -0,0 +1,5 @@
- type: run_sql
args:
cascade: false
read_only: false
sql: DROP TABLE "public"."relatedjobs";

View File

@@ -0,0 +1,31 @@
- type: run_sql
args:
cascade: false
read_only: false
sql: CREATE EXTENSION IF NOT EXISTS pgcrypto;
- type: run_sql
args:
cascade: false
read_only: false
sql: |-
CREATE TABLE "public"."relatedjobs"("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "parentjob" uuid NOT NULL, "childjob" UUID NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("parentjob") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("childjob") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade, UNIQUE ("id"));
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_relatedjobs_updated_at"
BEFORE UPDATE ON "public"."relatedjobs"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_relatedjobs_updated_at" ON "public"."relatedjobs"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
- type: add_existing_table_or_view
args:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,24 @@
- type: drop_relationship
args:
relationship: relatedjobs
table:
name: jobs
schema: public
- type: drop_relationship
args:
relationship: relatedjobsByChildjob
table:
name: jobs
schema: public
- type: drop_relationship
args:
relationship: job
table:
name: relatedjobs
schema: public
- type: drop_relationship
args:
relationship: jobByChildjob
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,40 @@
- type: create_array_relationship
args:
name: relatedjobs
table:
name: jobs
schema: public
using:
foreign_key_constraint_on:
column: parentjob
table:
name: relatedjobs
schema: public
- type: create_array_relationship
args:
name: relatedjobsByChildjob
table:
name: jobs
schema: public
using:
foreign_key_constraint_on:
column: childjob
table:
name: relatedjobs
schema: public
- type: create_object_relationship
args:
name: job
table:
name: relatedjobs
schema: public
using:
foreign_key_constraint_on: parentjob
- type: create_object_relationship
args:
name: jobByChildjob
table:
name: relatedjobs
schema: public
using:
foreign_key_constraint_on: childjob

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: parentjob_rel
new_name: job
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: job
new_name: parentjob_rel
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: childjob_rel
new_name: jobByChildjob
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: jobByChildjob
new_name: childjob_rel
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: relatedjobs_parent
new_name: relatedjobs
table:
name: jobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: relatedjobs
new_name: relatedjobs_parent
table:
name: jobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: relatedjobs_child
new_name: relatedjobsByChildjob
table:
name: jobs
schema: public

View File

@@ -0,0 +1,7 @@
- type: rename_relationship
args:
name: relatedjobsByChildjob
new_name: relatedjobs_child
table:
name: jobs
schema: public

View File

@@ -0,0 +1,6 @@
- type: drop_insert_permission
args:
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,36 @@
- type: create_insert_permission
args:
permission:
allow_upsert: true
backend_only: false
check:
_or:
- parentjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
- childjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- updated_at
- parentjob
- childjob
set: {}
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,6 @@
- type: drop_select_permission
args:
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,37 @@
- type: create_select_permission
args:
permission:
allow_aggregations: false
backend_only: false
columns:
- created_at
- updated_at
- childjob
- id
- parentjob
computed_fields: []
filter:
_or:
- parentjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
- childjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
limit: null
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,6 @@
- type: drop_update_permission
args:
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,35 @@
- type: create_update_permission
args:
permission:
backend_only: false
columns:
- created_at
- updated_at
- childjob
- id
- parentjob
filter:
_or:
- parentjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
- childjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
set: {}
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,6 @@
- type: drop_delete_permission
args:
role: user
table:
name: relatedjobs
schema: public

View File

@@ -0,0 +1,28 @@
- type: create_delete_permission
args:
permission:
backend_only: false
filter:
_or:
- parentjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
- childjob_rel:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
role: user
table:
name: relatedjobs
schema: public

View File

@@ -721,6 +721,13 @@ tables:
table:
schema: public
name: csiquestions
- name: dms_vehicles
using:
foreign_key_constraint_on:
column: bodyshopid
table:
schema: public
name: dms_vehicles
- name: documents
using:
foreign_key_constraint_on:
@@ -1527,6 +1534,87 @@ tables:
- active:
_eq: true
check: null
- table:
schema: public
name: dms_vehicles
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
columns:
- id
- created_at
- makecode
- modelcode
- make
- model
- bodyshopid
backend_only: false
select_permissions:
- role: user
permission:
columns:
- id
- created_at
- makecode
- modelcode
- make
- model
- bodyshopid
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
update_permissions:
- role: user
permission:
columns:
- make
- makecode
- model
- modelcode
- created_at
- bodyshopid
- id
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
check: null
delete_permissions:
- role: user
permission:
filter:
bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: documents
@@ -4554,6 +4642,9 @@ functions:
- function:
schema: public
name: search_cccontracts
- function:
schema: public
name: search_dms_vehicles
- function:
schema: public
name: search_exportlog

View File

@@ -3,8 +3,8 @@
"version": "0.0.1",
"license": "UNLICENSED",
"engines": {
"node": "12.18.3",
"npm": "6.11.3"
"node": "16.9.1",
"npm": "7.17.0"
},
"scripts": {
"setup": "yarn && cd client && yarn",
@@ -17,17 +17,19 @@
"start": "node server.js"
},
"dependencies": {
"aws-sdk": "^2.951.0",
"aws-sdk": "^2.979.0",
"bluebird": "^3.7.2",
"body-parser": "^1.18.3",
"cloudinary": "^1.26.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"cors": "2.8.5",
"csrf": "^3.1.0",
"dinero.js": "^1.9.0",
"dotenv": "10.0.0",
"express": "^4.16.4",
"firebase-admin": "^9.11.0",
"graphql": "^15.5.1",
"firebase-admin": "^9.11.1",
"graphql": "^15.5.3",
"graphql-request": "^3.4.0",
"graylog2": "^0.2.1",
"inline-css": "^3.0.0",
@@ -36,14 +38,16 @@
"moment": "^2.29.1",
"node-fetch": "^2.6.1",
"node-mailjet": "^3.3.4",
"node-quickbooks": "^2.0.39",
"nodemailer": "^6.6.3",
"phone": "^3.1.2",
"soap": "^0.40.0",
"socket.io": "^4.1.3",
"ssh2-sftp-client": "^7.0.0",
"stripe": "^8.164.0",
"twilio": "^3.66.0",
"xmlbuilder2": "^2.4.1"
"phone": "^3.1.6",
"query-string": "^7.0.1",
"soap": "^0.42.0",
"socket.io": "^4.2.0",
"ssh2-sftp-client": "^7.0.3",
"stripe": "^8.171.0",
"twilio": "^3.67.1",
"xmlbuilder2": "^3.0.2"
},
"devDependencies": {
"concurrently": "^6.2.0",

View File

@@ -7,6 +7,7 @@ const twilio = require("twilio");
const logger = require("./server/utils/logger");
global.fetch = require("node-fetch");
var fb = require("./server/firebase/firebase-handler");
var cookieParser = require("cookie-parser");
//var enforce = require("express-sslify");
@@ -21,8 +22,26 @@ const app = express();
const port = process.env.PORT || 5000;
//const port = 5000;
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server, {
path: "/ws",
cors: {
origin: [
"https://test.imex.online",
"http://localhost:3000",
"https://imex.online",
],
methods: ["GET", "POST"],
},
});
exports.io = io;
require("./server/web-sockets/web-socket");
//app.use(fb.validateFirebaseIdToken);
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
//app.use(enforce.HTTPS({ trustProtoHeader: true }));
@@ -127,8 +146,9 @@ var utils = require("./server/utils/utils");
app.post("/utils/time", utils.servertime);
var qbo = require("./server/accounting/qbo/qbo");
app.post("/qbo/authorize", qbo.authorize);
app.post("/qbo/authorize", fb.validateFirebaseIdToken, qbo.authorize);
app.get("/qbo/callback", qbo.callback);
app.post("/qbo/receivables", fb.validateFirebaseIdToken, qbo.receivables);
var data = require("./server/data/data");
app.post("/data/ah", data.autohouse);
@@ -136,25 +156,13 @@ app.post("/data/ah", data.autohouse);
var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default);
var cdkGetMake = require("./server/cdk/cdk-get-makes");
app.post("/cdk/getvehicles", fb.validateFirebaseIdToken, cdkGetMake.default);
app.get("/", async function (req, res) {
res.status(200).send("Access Forbidden.");
});
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server, {
path: "/ws",
cors: {
origin: [
"https://test.imex.online",
"http://localhost:3000",
"https://imex.online",
],
methods: ["GET", "POST"],
},
});
server.listen(port, (error) => {
if (error) throw error;
logger.log(
@@ -163,5 +171,3 @@ server.listen(port, (error) => {
"api"
);
});
exports.io = io;
require("./server/web-sockets/web-socket");

View File

@@ -6,26 +6,27 @@ require("dotenv").config({
),
});
const OAuthClient = require("intuit-oauth");
var Tokens = require("csrf");
var tokens = new Tokens();
const logger = require("../../utils/logger");
const oauthClient = new OAuthClient({
clientId: process.env.QB_ONLINE_CLIENT_ID,
clientSecret: process.env.QB_ONLINE_SECRET,
environment: "sandbox", //process.env.NODE_ENV === "production" ? "production" : "sandbox",
redirectUri: process.env.QB_ONLINE_REDIRECT_URI,
clientId: process.env.QBO_CLIENT_ID,
clientSecret: process.env.QBO_SECRET,
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
redirectUri: process.env.QBO_REDIRECT_URI,
});
exports.default = async (req, res) => {
console.log("QBO Authorize Called");
const { userId } = req.body;
console.log("exports.default -> userId", userId);
// AuthorizationUri
try {
logger.log("qbo-auth-uri", "DEBUG", req.user.email, null, null);
const authUri = oauthClient.authorizeUri({
scope: [OAuthClient.scopes.Accounting, OAuthClient.scopes.OpenId],
state: req.user.email,
}); // can be an array of multiple scopes ex : {scope:[OAuthClient.scopes.Accounting,OAuthClient.scopes.OpenId]}
const authUri = oauthClient.authorizeUri({
scope: [OAuthClient.scopes.Accounting, OAuthClient.scopes.OpenId],
state: tokens.create(userId),
}); // can be an array of multiple scopes ex : {scope:[OAuthClient.scopes.Accounting,OAuthClient.scopes.OpenId]}
console.log("authUri", authUri);
// Redirect the authUri
res.send(authUri);
res.send(authUri);
} catch (error) {
logger.log("qbo-auth-uri-error", "ERROR", req.user.email, null, { error });
res.status(500).json(error);
}
};

View File

@@ -5,32 +5,83 @@ require("dotenv").config({
`.env.${process.env.NODE_ENV || "development"}`
),
});
const logger = require("../../utils/logger");
const OAuthClient = require("intuit-oauth");
var Tokens = require("csrf");
var QuickBooks = require("node-quickbooks");
const Promise = require("bluebird");
const QuickBooksPromise = Promise.promisifyAll(QuickBooks.prototype);
const client = require("../../graphql-client/graphql-client").client;
const queries = require("../../graphql-client/queries");
const queryString = require("query-string");
const oauthClient = new OAuthClient({
clientId: process.env.QB_ONLINE_CLIENT_ID,
clientSecret: process.env.QB_ONLINE_SECRET,
environment: "sandbox", //process.env.NODE_ENV === "production" ? "production" : "sandbox",
redirectUri: process.env.QB_ONLINE_REDIRECT_URI,
clientId: process.env.QBO_CLIENT_ID,
clientSecret: process.env.QBO_SECRET,
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
redirectUri: process.env.QBO_REDIRECT_URI,
logging: true,
});
exports.default = async (req, res) => {
// Parse the redirect URL for authCode and exchange them for tokens
const parseRedirect = req.url;
const { code, state, realmId } = req.query;
console.log("exports.default -> state", state);
// Exchange the auth code retrieved from the **req.url** on the redirectUri
oauthClient
.createToken(parseRedirect)
.then(function (authResponse) {
console.log("The Token is " + JSON.stringify(authResponse.getJson()));
const { access_token, refresh_token } = authResponse.getJson();
console.log("exports.default -> refresh_token", refresh_token);
console.log("exports.default -> access_token", access_token);
})
.catch(function (e) {
console.error("The error message is :" + e.originalMessage);
console.error(e.intuit_tid);
const params = queryString.parse(req.url.split("?").reverse()[0]);
try {
logger.log("qbo-callback-create-token", "DEBUG", params.state, null, null);
const authResponse = await oauthClient.createToken(req.url);
if (authResponse.json.error) {
logger.log("qbo-callback-error", "ERROR", params.state, null, {
error: authResponse.json,
});
res.redirect(
`http://localhost:3000/manage/accounting/qbo?error=${encodeURIComponent(
JSON.stringify(authResponse.json)
)}`
);
} else {
await client.request(queries.SET_QBO_AUTH, {
email: params.state,
qbo_auth: { ...authResponse.json, createdAt: Date.now() },
});
logger.log(
"qbo-callback-create-token-success",
"DEBUG",
params.state,
null,
null
);
res.redirect(
`http://localhost:3000/manage/accounting/qbo?${queryString.stringify(
params
)}`
);
}
} catch (e) {
logger.log("qbo-callback-error", "ERROR", params.state, null, {
error: e,
});
res.status(400).json(e);
}
};
exports.refresh = async (oauthClient, req) => {
try {
logger.log("qbo-token-refresh", "DEBUG", req.user.email, null, null);
const authResponse = await oauthClient.refresh();
await client.request(queries.SET_QBO_AUTH, {
email: req.user.email,
qbo_auth: { ...authResponse.json, createdAt: Date.now() },
});
} catch (error) {
logger.log("qbo-token-refresh-error", "ERROR", req.user.email, null, {
error,
});
}
};
exports.setNewRefreshToken = async (email, apiResponse) => {
logger.log("qbo-token-updated", "DEBUG", email, null, null);
await client.request(queries.SET_QBO_AUTH, {
email,
qbo_auth: { ...apiResponse.token, createdAt: Date.now() },
});
};

View File

@@ -0,0 +1,692 @@
const urlBuilder = require("./qbo").urlBuilder;
const path = require("path");
require("dotenv").config({
path: path.resolve(
process.cwd(),
`.env.${process.env.NODE_ENV || "development"}`
),
});
const logger = require("../../utils/logger");
const Dinero = require("dinero.js");
const DineroQbFormat = require("../accounting-constants").DineroQbFormat;
const apiGqlClient = require("../../graphql-client/graphql-client").client;
const queries = require("../../graphql-client/queries");
const {
refresh: refreshOauthToken,
setNewRefreshToken,
} = require("./qbo-callback");
const OAuthClient = require("intuit-oauth");
const GraphQLClient = require("graphql-request").GraphQLClient;
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
exports.default = async (req, res) => {
const oauthClient = new OAuthClient({
clientId: process.env.QBO_CLIENT_ID,
clientSecret: process.env.QBO_SECRET,
environment:
process.env.NODE_ENV === "production" ? "production" : "sandbox",
redirectUri: process.env.QBO_REDIRECT_URI,
logging: true,
});
try {
//Fetch the API Access Tokens & Set them for the session.
const response = await apiGqlClient.request(queries.GET_QBO_AUTH, {
email: req.user.email,
});
oauthClient.setToken(response.associations[0].qbo_auth);
const getToken = oauthClient.getToken();
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { jobIds } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, {
ids: ["966dc7f9-2acd-44dc-9df5-d07c5578070a"],
//jobIds
});
const { jobs, bodyshops } = result;
const job = jobs[0];
const bodyshop = bodyshops[0];
const isThreeTier = bodyshop.accountingconfig.tiers === 3;
const twoTierPref = bodyshop.accountingconfig.twotierpref;
//Replace this with a for-each loop to check every single Job that's included in the list.
let insCoCustomerTier, ownerCustomerTier, jobTier;
if (isThreeTier || twoTierPref === "source") {
//Insert the insurance company tier.
//Query for top level customer, the insurance company name.
insCoCustomerTier = await QueryInsuranceCo(oauthClient, req, job);
if (!insCoCustomerTier) {
//Creating the Insurance Customer.
insCoCustomerTier = await InsertInsuranceCo(
oauthClient,
req,
job,
bodyshop
);
}
}
if (isThreeTier || twoTierPref === "name") {
//Insert the name/owner and account for whether the source should be the ins co in 3 tier..
ownerCustomerTier = await QueryOwner(oauthClient, req, job);
//Query for the owner itself.
if (!ownerCustomerTier) {
ownerCustomerTier = await InsertOwner(
oauthClient,
req,
job,
isThreeTier,
insCoCustomerTier
);
}
}
//Query for the Job or Create it.
jobTier = await QueryJob(oauthClient, req, job);
// Need to validate that the job tier is associated to the right individual?
if (!jobTier) {
jobTier = await InsertJob(
oauthClient,
req,
job,
isThreeTier,
ownerCustomerTier
);
}
await InsertInvoice(oauthClient, req, job, bodyshop, jobTier);
res.sendStatus(200);
} catch (error) {
console.log(error);
res.status(400).json(error);
}
};
async function QueryInsuranceCo(oauthClient, req, job) {
try {
const result = await oauthClient.makeApiCall({
url: urlBuilder(
req.cookies.qbo_realmId,
"query",
`select * From Customer where DisplayName = '${job.ins_co_nm}'`
),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
setNewRefreshToken(req.user.email, result);
return (
result.json &&
result.json.QueryResponse &&
result.json.QueryResponse.Customer &&
result.json.QueryResponse.Customer[0]
);
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "QueryInsuranceCo",
});
throw error;
}
}
async function InsertInsuranceCo(oauthClient, req, job, bodyshop) {
const insCo = bodyshop.md_ins_cos.find((i) => i.name === job.ins_co_nm);
const Customer = {
DisplayName: job.ins_co_nm,
BillAddr: {
City: job.ownr_city,
Line1: insCo.street1,
Line2: insCo.street2,
PostalCode: insCo.zip,
CountrySubDivisionCode: insCo.state,
},
};
try {
const result = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "customer"),
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Customer),
});
setNewRefreshToken(req.user.email, result);
return result && result.Customer;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertInsuranceCo",
});
throw error;
}
}
async function QueryOwner(oauthClient, req, job) {
const ownerName = generateOwnerTier(job, true, null);
const result = await oauthClient.makeApiCall({
url: urlBuilder(
req.cookies.qbo_realmId,
"query",
`select * From Customer where DisplayName = '${ownerName}'`
),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
setNewRefreshToken(req.user.email, result);
return (
result.json &&
result.json.QueryResponse &&
result.json.QueryResponse.Customer &&
result.json.QueryResponse.Customer[0]
);
}
async function InsertOwner(oauthClient, req, job, isThreeTier, parentTierRef) {
const ownerName = generateOwnerTier(job, true, null);
const Customer = {
DisplayName: ownerName,
BillAddr: {
City: job.ownr_city,
Line1: job.ownr_addr1,
Line2: job.ownr_addr2,
PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st,
},
...(isThreeTier
? {
Job: true,
ParentRef: {
value: parentTierRef.Id,
},
}
: {}),
};
try {
const result = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "customer"),
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Customer),
});
setNewRefreshToken(req.user.email, result);
return result && result.Customer;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertOwner",
});
throw error;
}
}
async function QueryJob(oauthClient, req, job) {
const result = await oauthClient.makeApiCall({
url: urlBuilder(
req.cookies.qbo_realmId,
"query",
`select * From Customer where DisplayName = '${job.ro_number}'`
),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
setNewRefreshToken(req.user.email, result);
return (
result.json &&
result.json.QueryResponse &&
result.json.QueryResponse.Customer &&
result.json.QueryResponse.Customer[0]
);
}
async function InsertJob(oauthClient, req, job, isThreeTier, parentTierRef) {
const Customer = {
DisplayName: job.ro_number,
BillAddr: {
City: job.ownr_city,
Line1: job.ownr_addr1,
Line2: job.ownr_addr2,
PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st,
},
...(isThreeTier
? {
Job: true,
ParentRef: {
value: parentTierRef.Id,
},
}
: {}),
};
try {
const result = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "customer"),
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(Customer),
});
setNewRefreshToken(req.user.email, result);
return result && result.Customer;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertOwner",
});
throw error;
}
}
async function QueryMetaData(oauthClient, req) {
const items = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Item`),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
setNewRefreshToken(req.user.email, items);
const taxCodes = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From TaxCode`),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const taxCodeMapping = {};
const accounts = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Account`),
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
taxCodes.json &&
taxCodes.json.QueryResponse &&
taxCodes.json.QueryResponse.TaxCode.forEach((t) => {
taxCodeMapping[t.Name] = t.Id;
});
const itemMapping = {};
items.json &&
items.json.QueryResponse &&
items.json.QueryResponse.Item.forEach((t) => {
itemMapping[t.Name] = t.Id;
});
return {
items: itemMapping,
taxCodes: taxCodeMapping,
};
}
async function InsertInvoice(oauthClient, req, job, bodyshop, parentTierRef) {
const { items, taxCodes } = await QueryMetaData(oauthClient, req);
const InvoiceLineAdd = [];
const responsibilityCenters = bodyshop.md_responsibility_centers;
const invoiceLineHash = {}; //The hash of cost and profit centers based on the center name.
//Determine if there are MAPA and MASH lines already on the estimate.
//If there are, don't do anything extra (mitchell estimate)
//Otherwise, calculate them and add them to the default MAPA and MASH centers.
let hasMapaLine = false;
let hasMashLine = false;
//Create the invoice lines mapping.
job.joblines.map((jobline) => {
//Parts Lines
if (jobline.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals/job-costing calculations.
hasMapaLine = true;
}
if (jobline.db_ref === "936007") {
hasMashLine = true;
}
if (jobline.profitcenter_part && jobline.act_price) {
let DineroAmount = Dinero({
amount: Math.round(jobline.act_price * 100),
}).multiply(jobline.part_qty || 1);
if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) {
// console.log("Have a part discount", jobline);
DineroAmount = DineroAmount.add(
DineroAmount.percentage(jobline.prt_dsmk_p || 0)
);
}
const account = responsibilityCenters.profits.find(
(i) => jobline.profitcenter_part.toLowerCase() === i.name.toLowerCase()
);
if (!account) {
logger.log(
"qbxml-receivables-no-account",
"warn",
null,
jobline.id,
null
);
throw new Error(
`A matching account does not exist for the part allocation. Center: ${jobline.profitcenter_part}`
);
}
if (!invoiceLineHash[account.name]) {
invoiceLineHash[account.name] = {
ItemRef: { FullName: account.accountitem },
Desc: account.accountdesc,
Quantity: 1, //jobline.part_qty,
Amount: DineroAmount, //.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
};
} else {
invoiceLineHash[account.name].Amount =
invoiceLineHash[account.name].Amount.add(DineroAmount);
}
}
// Labor Lines
if (
jobline.profitcenter_labor &&
jobline.mod_lb_hrs &&
jobline.mod_lb_hrs > 0
) {
const DineroAmount = Dinero({
amount: Math.round(
job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100
),
}).multiply(jobline.mod_lb_hrs);
const account = responsibilityCenters.profits.find(
(i) => jobline.profitcenter_labor.toLowerCase() === i.name.toLowerCase()
);
if (!account) {
throw new Error(
`A matching account does not exist for the labor allocation. Center: ${jobline.profitcenter_labor}`
);
}
if (!invoiceLineHash[account.name]) {
invoiceLineHash[account.name] = {
ItemRef: { FullName: account.accountitem },
Desc: account.accountdesc,
Quantity: 1, // jobline.mod_lb_hrs,
Amount: DineroAmount,
//Amount: DineroAmount.toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
};
} else {
invoiceLineHash[account.name].Amount =
invoiceLineHash[account.name].Amount.add(DineroAmount);
}
}
});
// console.log("Done creating hash", JSON.stringify(invoiceLineHash));
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
// console.log("Adding MAPA Line Manually.");
const mapaAccountName = responsibilityCenters.defaults.profits.MAPA;
const mapaAccount = responsibilityCenters.profits.find(
(c) => c.name === mapaAccountName
);
if (mapaAccount) {
InvoiceLineAdd.push({
ItemRef: { FullName: mapaAccount.accountitem },
Desc: mapaAccount.accountdesc,
Quantity: 1,
Amount: Dinero(job.job_totals.rates.mapa.total).toFormat(
DineroQbFormat
),
SalesTaxCodeRef: {
FullName: "E",
},
});
} else {
//console.log("NO MAPA ACCOUNT FOUND!!");
}
}
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
// console.log("Adding MASH Line Manually.");
const mashAccountName = responsibilityCenters.defaults.profits.MASH;
const mashAccount = responsibilityCenters.profits.find(
(c) => c.name === mashAccountName
);
if (mashAccount) {
InvoiceLineAdd.push({
ItemRef: { FullName: mashAccount.accountitem },
Desc: mashAccount.accountdesc,
Quantity: 1,
Amount: Dinero(job.job_totals.rates.mash.total).toFormat(
DineroQbFormat
),
SalesTaxCodeRef: {
FullName: "E",
},
});
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
//Convert the hash to an array.
Object.keys(invoiceLineHash).forEach((key) => {
InvoiceLineAdd.push({
...invoiceLineHash[key],
Amount: invoiceLineHash[key].Amount.toFormat(DineroQbFormat),
});
});
//Add Towing, storage and adjustment lines.
if (job.towing_payable && job.towing_payable !== 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
).accountitem,
},
Desc: "Towing",
Quantity: 1,
Amount: Dinero({
amount: Math.round((job.towing_payable || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
if (job.storage_payable && job.storage_payable !== 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
).accountitem,
},
Desc: "Storage",
Quantity: 1,
Amount: Dinero({
amount: Math.round((job.storage_payable || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["PAO"]
).accountitem,
},
Desc: "Adjustment",
Quantity: 1,
Amount: Dinero({
amount: Math.round((job.adjustment_bottom_line || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
//Add tax lines
const job_totals = job.job_totals;
const federal_tax = Dinero(job_totals.totals.federal_tax);
const state_tax = Dinero(job_totals.totals.state_tax);
const local_tax = Dinero(job_totals.totals.local_tax);
if (federal_tax.getAmount() > 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.federal.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.federal.accountdesc,
Amount: federal_tax.toFormat(DineroQbFormat),
});
}
if (state_tax.getAmount() > 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.state.accountdesc,
Amount: state_tax.toFormat(DineroQbFormat),
});
}
if (local_tax.getAmount() > 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.local.accountitem,
},
Desc: bodyshop.md_responsibility_centers.taxes.local.accountdesc,
Amount: local_tax.toFormat(DineroQbFormat),
});
}
//Region Specific
const { ca_bc_pvrt } = job;
if (ca_bc_pvrt) {
InvoiceLineAdd.push({
ItemRef: {
FullName: bodyshop.md_responsibility_centers.taxes.state.accountitem,
},
Desc: "PVRT",
Amount: Dinero({ amount: (ca_bc_pvrt || 0) * 100 }).toFormat(
DineroQbFormat
),
});
}
//map each invoice line to the correct style for QBO.
const invoiceObj = {
Line: [
{
DetailType: "SalesItemLineDetail",
Amount: 100,
SalesItemLineDetail: {
// ItemRef: {
// // name: "Services",
// value: "16",
// },
TaxCodeRef: {
value: "2",
},
Qty: 1,
UnitPrice: 100,
},
},
],
// Line: InvoiceLineAdd.map((i) => {
// return {
// DetailType: "SalesItemLineDetail",
// Amount: i.Amount,
// SalesItemLineDetail: {
// ItemRef: {
// //name: "Services",
// value: items[i.ItemRef.FullName],
// },
// // TaxCodeRef: {
// // value: "2",
// // },
// Qty: 1,
// },
// };
// }),
TxnTaxDetail: {
TaxLine: [
{
DetailType: "TaxLineDetail",
TaxLineDetail: {
NetAmountTaxable: 100,
TaxPercent: 7,
TaxRateRef: {
value: "16",
},
PercentBased: true,
},
},
],
},
CustomerRef: {
value: parentTierRef.Id,
},
};
try {
const result = await oauthClient.makeApiCall({
url: urlBuilder(req.cookies.qbo_realmId, "invoice"),
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(invoiceObj),
});
setNewRefreshToken(req.user.email, result);
return result && result.Invoice;
} catch (error) {
logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, {
error,
method: "InsertOwner",
});
throw error;
}
}

View File

@@ -1,7 +1,3 @@
exports.callback = require("./qbo-callback").default;
exports.authorize = require("./qbo-authorize").default;
const OAuthClient = require("intuit-oauth");
const path = require("path");
require("dotenv").config({
path: path.resolve(
@@ -9,3 +5,19 @@ require("dotenv").config({
`.env.${process.env.NODE_ENV || "development"}`
),
});
function urlBuilder(realmId, object, query = null) {
return `https://${
process.env.NODE_ENV === "development" || !process.env.NODE_ENV
? "sandbox-"
: ""
}quickbooks.api.intuit.com/v3/company/${realmId}/${object}${
query ? `?query=${encodeURIComponent(query)}` : ""
}`;
}
exports.urlBuilder = urlBuilder;
exports.callback = require("./qbo-callback").default;
exports.authorize = require("./qbo-authorize").default;
exports.refresh = require("./qbo-callback").refresh;
exports.receivables = require("./qbo-receivables").default;

View File

@@ -257,7 +257,9 @@ const generateInvoiceQbxml = (
if (jobline.prt_dsmk_p && jobline.prt_dsmk_p !== 0) {
// console.log("Have a part discount", jobline);
DineroAmount = DineroAmount.add(
DineroAmount.percentage(jobline.prt_dsmk_p || 0)
DineroAmount.percentage(Math.abs(jobline.prt_dsmk_p || 0)).multiply(
jobline.prt_dsmk_p > 0 ? 1 : -1
)
);
}
const account = responsibilityCenters.profits.find(
@@ -292,11 +294,7 @@ const generateInvoiceQbxml = (
}
}
// Labor Lines
if (
jobline.profitcenter_labor &&
jobline.mod_lb_hrs &&
jobline.mod_lb_hrs > 0
) {
if (jobline.profitcenter_labor && jobline.mod_lb_hrs) {
const DineroAmount = Dinero({
amount: Math.round(
jobs_by_pk[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] * 100
@@ -389,6 +387,63 @@ const generateInvoiceQbxml = (
});
});
//Add Towing, storage and adjustment lines.
if (jobs_by_pk.towing_payable && jobs_by_pk.towing_payable !== 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
).accountitem,
},
Desc: "Towing",
Quantity: 1,
Amount: Dinero({
amount: Math.round((jobs_by_pk.towing_payable || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
if (jobs_by_pk.storage_payable && jobs_by_pk.storage_payable !== 0) {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["TOW"]
).accountitem,
},
Desc: "Storage",
Quantity: 1,
Amount: Dinero({
amount: Math.round((jobs_by_pk.storage_payable || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
if (
jobs_by_pk.adjustment_bottom_line &&
jobs_by_pk.adjustment_bottom_line !== 0
) {
InvoiceLineAdd.push({
ItemRef: {
FullName: responsibilityCenters.profits.find(
(c) => c.name === responsibilityCenters.defaults.profits["PAO"]
).accountitem,
},
Desc: "Adjustment",
Quantity: 1,
Amount: Dinero({
amount: Math.round((jobs_by_pk.adjustment_bottom_line || 0) * 100),
}).toFormat(DineroQbFormat),
SalesTaxCodeRef: {
FullName: "E",
},
});
}
//Add tax lines
const job_totals = jobs_by_pk.job_totals;

View File

@@ -47,16 +47,40 @@ exports.default = async function (socket, jobid) {
},
};
//Determine if there are MAPA and MASH lines already on the estimate.
//If there are, don't do anything extra (mitchell estimate)
//Otherwise, calculate them and add them to the default MAPA and MASH centers.
let hasMapaLine = false;
let hasMashLine = false;
const profitCenterHash = job.joblines.reduce((acc, val) => {
//Check the Parts Assignment
if (val.db_ref === "936008") {
//If either of these DB REFs change, they also need to change in job-totals/job-costing calculations.
hasMapaLine = true;
}
if (val.db_ref === "936007") {
hasMashLine = true;
}
if (val.profitcenter_part) {
if (!acc[val.profitcenter_part]) acc[val.profitcenter_part] = Dinero();
acc[val.profitcenter_part] = acc[val.profitcenter_part].add(
Dinero({
amount: Math.round((val.act_price || 0) * 100),
}).multiply(val.part_qty || 0)
);
let DineroAmount = Dinero({
amount: Math.round(val.act_price * 100),
}).multiply(val.part_qty || 1);
if (val.prt_dsmk_p && val.prt_dsmk_p !== 0) {
// console.log("Have a part discount", val);
DineroAmount = DineroAmount.add(
DineroAmount.percentage(Math.abs(val.prt_dsmk_p || 0)).multiply(
val.prt_dsmk_p > 0 ? 1 : -1
)
);
}
acc[val.profitcenter_part] =
acc[val.profitcenter_part].add(DineroAmount);
}
if (val.profitcenter_labor) {
//Check the Labor Assignment.
@@ -79,7 +103,8 @@ exports.default = async function (socket, jobid) {
bill_val.billlines.map((line_val) => {
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
const lineDinero = Dinero({
let lineDinero = Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
@@ -87,32 +112,73 @@ exports.default = async function (socket, jobid) {
bill_acc[line_val.cost_center] =
bill_acc[line_val.cost_center].add(lineDinero);
//Add appropriate tax amounts.
const {
applicable_taxes: { local, state, federal },
} = line_val;
if (local) {
taxAllocations.local.cost = taxAllocations.local.cost.add(
lineDinero.percentage(bill_val.local_tax_rate || 0)
);
}
if (state) {
taxAllocations.state.cost = taxAllocations.state.cost.add(
lineDinero.percentage(bill_val.state_tax_rate || 0)
);
}
if (federal) {
taxAllocations.federal.cost = taxAllocations.federal.cost.add(
lineDinero.percentage(bill_val.federal_tax_rate || 0)
);
}
return null;
});
return bill_acc;
}, {});
job.timetickets.forEach((ticket) => {
//Get the total amount of the ticket.
let TicketTotal = Dinero({
amount: Math.round(
ticket.rate *
(ticket.employee && ticket.employee.flat_rate
? ticket.productivehrs || 0
: ticket.actualhrs || 0) *
100
),
});
//Add it to the right cost center.
if (!costCenterHash[ticket.cost_center])
costCenterHash[ticket.cost_center] = Dinero();
costCenterHash[ticket.cost_center] =
costCenterHash[ticket.cost_center].add(TicketTotal);
});
if (!hasMapaLine && job.job_totals.rates.mapa.total.amount > 0) {
// console.log("Adding MAPA Line Manually.");
const mapaAccountName =
bodyshop.md_responsibility_centers.defaults.profits.MAPA;
const mapaAccount = bodyshop.md_responsibility_centers.profits.find(
(c) => c.name === mapaAccountName
);
if (mapaAccount) {
if (!profitCenterHash[mapaAccountName])
profitCenterHash[mapaAccountName] = Dinero();
profitCenterHash[mapaAccountName] = profitCenterHash[
mapaAccountName
].add(Dinero(job.job_totals.rates.mapa.total));
} else {
//console.log("NO MAPA ACCOUNT FOUND!!");
}
}
if (!hasMashLine && job.job_totals.rates.mash.total.amount > 0) {
// console.log("Adding MASH Line Manually.");
const mashAccountName =
bodyshop.md_responsibility_centers.defaults.profits.MASH;
const mashAccount = bodyshop.md_responsibility_centers.profits.find(
(c) => c.name === mashAccountName
);
if (mashAccount) {
if (!profitCenterHash[mashAccountName])
profitCenterHash[mashAccountName] = Dinero();
profitCenterHash[mashAccountName] = profitCenterHash[
mashAccountName
].add(Dinero(job.job_totals.rates.mash.total));
} else {
// console.log("NO MASH ACCOUNT FOUND!!");
}
}
const jobAllocations = _.union(
Object.keys(profitCenterHash),
Object.keys(costCenterHash)
@@ -141,7 +207,9 @@ exports.default = async function (socket, jobid) {
taxAllocations[key].sale.getAmount() > 0 ||
taxAllocations[key].cost.getAmount() > 0
)
.map((key) => taxAllocations[key]),
.map((key) => {
return { ...taxAllocations[key], tax: key };
}),
];
} catch (error) {
CdkBase.createLogEvent(

View File

@@ -8,39 +8,107 @@ require("dotenv").config({
const GraphQLClient = require("graphql-request").GraphQLClient;
const soap = require("soap");
const queries = require("../graphql-client/queries");
const CdkBase = require("../web-sockets/web-socket");
const CdkWsdl = require("./cdk-wsdl").default;
const logger = require("../utils/logger");
const Dinero = require("dinero.js");
const _ = require("lodash");
const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
const { performance } = require("perf_hooks");
exports.default = async function (socket, cdk_dealerid) {
const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
// exports.default = async function (socket, cdk_dealerid) {
// try {
// CdkBase.createLogEvent(
// socket,
// "DEBUG",
// `Getting makes and models list from CDK.`
// );
// return await GetCdkMakes(socket, cdk_dealerid);
// } catch (error) {
// CdkBase.createLogEvent(
// socket,
// "ERROR",
// `Error encountered in CdkGetMakes. ${error}`
// );
// }
// };
exports.default = async function ReloadCdkMakes(req, res) {
const { bodyshopid, cdk_dealerid } = req.body;
try {
CdkBase.createLogEvent(
socket,
const BearerToken = req.headers.authorization;
//Query all CDK Models
const newList = await GetCdkMakes(req, cdk_dealerid);
console.log("🚀 ~ file: cdk-get-makes.js ~ line 40 ~ newList", newList);
//Clear out the existing records
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
const deleteResult = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.DELETE_ALL_DMS_VEHICLES, {});
console.log(
"🚀 ~ file: cdk-get-makes.js ~ line 53 ~ deleteResult",
deleteResult
);
//Insert the new ones.
const insertResult = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_DMS_VEHICLES, {
vehicles: newList.map((i) => {
return {
bodyshopid,
makecode: i.makeCode,
modelcode: i.modelCode,
make: i.makeFullName,
model: i.modelFullName,
};
}),
});
console.log(
"🚀 ~ file: cdk-get-makes.js ~ line 66 ~ insertResult",
insertResult
);
logger.log(
"cdk-replace-makes-models-success",
"DEBUG",
`Getting makes and models list from CDK.`
req.user.email,
null,
{
cdk_dealerid,
count: newList.length,
}
);
return await GetCdkMakes(socket, cdk_dealerid);
res.sendStatus(200);
} catch (error) {
CdkBase.createLogEvent(
socket,
logger.log(
"cdk-replace-makes-models-error",
"ERROR",
`Error encountered in CdkGetMakes. ${error}`
req.user.email,
null,
{
cdk_dealerid,
error,
}
);
res.status(500).json(error);
}
};
async function GetCdkMakes(socket, cdk_dealerid) {
CdkBase.createLogEvent(socket, "TRACE", `{1} Begin GetCDkMakes WSDL Call`);
async function GetCdkMakes(req, cdk_dealerid) {
logger.log("cdk-replace-makes-models", "DEBUG", req.user.email, null, {
cdk_dealerid,
});
try {
const soapClientVehicleInsert = await soap.createClientAsync(
CdkWsdl.VehicleInsert
CdkWsdl.VehicleInsertUpdate
);
const start = performance.now();
const soapResponseVehicleSearch =
await soapClientVehicleInsert.getMakeModelAsync(
@@ -51,28 +119,25 @@ async function GetCdkMakes(socket, cdk_dealerid) {
{}
);
CheckCdkResponseForError(socket, soapResponseVehicleSearch);
CheckCdkResponseForError(null, soapResponseVehicleSearch);
const [
result, //rawResponse, soapheader, rawRequest
] = soapResponseVehicleSearch;
const end = performance.now();
CdkBase.createLogEvent(
socket,
"TRACE",
`soapClientVehicleInsert.getMakeModelAsync Result Length ${
result.return.length
} and took ${end - start}ms`
return result.return;
} catch (error) {
logger.log(
"cdk-replace-makes-models-error",
"ERROR",
req.user.email,
null,
{
cdk_dealerid,
error,
}
);
return result.return.map((element, index) => {
return { id: index, ...element };
});
} catch (error) {
CdkBase.createLogEvent(
socket,
"ERROR",
`Error in GetCdkMakes - ${JSON.stringify(error, null, 2)}`
);
throw new Error(error);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -78,11 +78,13 @@ exports.checkIndividualResult = checkIndividualResult;
const cdkDomain = "https://uat-3pa.dmotorworks.com";
exports.default = {
// VehicleSearch: `${cdkDomain}/pip-vehicle/services/VehicleSearch?wsdl`,
HelpDataBase: `${cdkDomain}/pip-help-database-location/services/HelpDatabaseLocation?wsdl`,
AccountingGLInsertUpdate: `${cdkDomain}/pip-accounting-gl/services/AccountingGLInsertUpdate?wsdl`,
VehicleInsertUpdate: `${cdkDomain}/pip-vehicle/services/VehicleInsertUpdate?wsdl`,
CustomerInsertUpdate: `${cdkDomain}/pip-customer/services/CustomerInsertUpdate?wsdl`,
CustomerSearch: `${cdkDomain}/pip-customer/services/CustomerSearch?wsdl`,
VehicleSearch: `${cdkDomain}/pip-vehicle/services/VehicleSearch?wsdl`,
VehicleInsert: `${cdkDomain}/pip-vehicle/services/VehicleInsertUpdate?wsdl`,
ServiceHistoryInsert: `${cdkDomain}/pip-service-history-insert/services/ServiceHistoryInsert?wsdl`,
};
// The following login credentials will be used for all PIPs and all environments (User Acceptance Testing and Production).

View File

@@ -53,7 +53,6 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
ro_number
clm_total
clm_no
invoice_allocation
ownerid
ownr_ln
ownr_fn
@@ -92,6 +91,9 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
class
ca_bc_pvrt
ca_customer_gst
towing_payable
storage_payable
adjustment_bottom_line
owner {
accountingid
}
@@ -114,6 +116,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
id
md_responsibility_centers
accountingconfig
md_ins_cos
}
}
`;
@@ -133,9 +136,11 @@ query QUERY_JOBS_FOR_CDK_EXPORT($id: uuid!) {
ownr_fn
ownr_addr1
ownr_addr2
ownr_ph1
ownr_zip
ownr_city
ownr_st
ownr_ea
ins_co_nm
job_totals
rate_la1
@@ -173,9 +178,11 @@ query QUERY_JOBS_FOR_CDK_EXPORT($id: uuid!) {
ca_customer_gst
bodyshop {
id
md_ro_statuses
md_responsibility_centers
accountingconfig
cdk_dealerid
cdk_configuration
}
owner {
accountingid
@@ -933,105 +940,169 @@ exports.GET_AUTOHOUSE_SHOPS = `query GET_AUTOHOUSE_SHOPS {
}
}
`;
exports.DELETE_ALL_DMS_VEHICLES = `mutation DELETE_ALL_DMS_VEHICLES{
delete_dms_vehicles(where: {}) {
affected_rows
}
}
`;
exports.INSERT_DMS_VEHICLES = `mutation INSERT_DMS_VEHICLES($vehicles: [dms_vehicles_insert_input!]!) {
insert_dms_vehicles(objects: $vehicles) {
affected_rows
}
}
exports.GET_CDK_ALLOCATIONS = `
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop{
id
md_responsibility_centers
}
ro_number
invoice_allocation
ins_co_id
`;
exports.GET_CDK_ALLOCATIONS = `query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
bodyshop {
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
status
date_exported
date_invoiced
voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
applicable_taxes
}
}
joblines(where: { removed: { _eq: false } }) {
id
removed
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
profitcenter_labor
profitcenter_part
prt_dsmk_p
md_responsibility_centers
cdk_configuration
}
ro_number
invoice_allocation
ins_co_id
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
status
date_exported
date_invoiced
voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
timetickets {
id
actualhrs
cost_center
productivehrs
rate
employee {
flat_rate
}
}
bills(where: {isinhouse: {_eq: false}}) {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
applicable_taxes
}
}
joblines(where: {removed: {_eq: false}}) {
id
removed
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
part_type
oem_partno
db_price
db_ref
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
profitcenter_labor
profitcenter_part
prt_dsmk_p
}
}
}
`;
exports.GET_QBO_AUTH = `query GET_QBO_AUTH($email: String!) {
associations(where: {_and: {active: {_eq: true}, useremail: {_eq: $email}}}){
id
qbo_auth
}
}`;
exports.SET_QBO_AUTH = `mutation SET_QBO_AUTH($email: String!, $qbo_auth: jsonb!) {
update_associations(_set: {qbo_auth: $qbo_auth}, where: {_and: {active: {_eq: true}, useremail: {_eq: $email}}}){
affected_rows
}
}
`;
exports.MARK_JOB_EXPORTED = `
mutation MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $log: exportlog_insert_input!) {
update_jobs(where: {id: {_eq: $jobId}}, _set: $job) {
returning {
id
date_exported
status
alt_transport
ro_number
production_vars
lbr_adjustments
}
}
insert_exportlog_one(object: $log) {
id
}
}
`;
exports.INSERT_EXPORT_LOG = `
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
insert_exportlog_one(object: $log) {
id
}
}
`;

View File

@@ -301,7 +301,8 @@ function GenerateCostingData(job) {
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 0)
.percentage(val.prt_dsmk_p || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
);
if (!acc.parts[partsProfitCenter])
acc.parts[partsProfitCenter] = Dinero();
@@ -330,7 +331,8 @@ function GenerateCostingData(job) {
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 0)
.percentage(val.prt_dsmk_p || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
);
if (!acc.parts[partsProfitCenter])

View File

@@ -242,7 +242,16 @@ function CalculatePartsTotals(jobLines) {
subtotal: acc.sublets.subtotal.add(
Dinero({
amount: Math.round(value.act_price * 100),
}).multiply(value.part_qty || 0)
})
.multiply(value.part_qty || 0)
.add(
Dinero({
amount: Math.round(value.act_price * 100),
})
.multiply(value.part_qty || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
)
),
},
};
@@ -258,7 +267,8 @@ function CalculatePartsTotals(jobLines) {
amount: Math.round((value.act_price || 0) * 100),
})
.multiply(value.part_qty || 0)
.percentage(value.prt_dsmk_p || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
),
list: {
...acc.parts.list,
@@ -289,7 +299,8 @@ function CalculatePartsTotals(jobLines) {
amount: Math.round((value.act_price || 0) * 100),
})
.multiply(value.part_qty || 0)
.percentage(value.prt_dsmk_p || 0)
.percentage(Math.abs(value.prt_dsmk_p || 0))
.multiply(value.prt_dsmk_p > 0 ? 1 : -1)
),
},
};
@@ -304,6 +315,7 @@ function CalculatePartsTotals(jobLines) {
},
sublets: {
subtotal: Dinero({ amount: 0 }),
total: Dinero({ amount: 0 }),
},
}
@@ -425,7 +437,8 @@ function CalculateTaxesTotals(job, otherTotals) {
amount: Math.round((val.act_price || 0) * 100),
})
.multiply(val.part_qty || 0)
.percentage(val.prt_dsmk_p || 0)
.percentage(Math.abs(val.prt_dsmk_p || 0))
.multiply(val.prt_dsmk_p > 0 ? 1 : -1)
)
.percentage(
((job.parts_tax_rates &&

View File

@@ -8,7 +8,10 @@ require("dotenv").config({
const { io } = require("../../server");
const { admin } = require("../firebase/firebase-handler");
const CdkJobExport = require("../cdk/cdk-job-export").default;
const {
default: CdkJobExport,
CdkSelectedCustomer,
} = require("../cdk/cdk-job-export");
const CdkGetMakes = require("../cdk/cdk-get-makes").default;
const CdkCalculateAllocations =
require("../cdk/cdk-calculate-allocations").default;
@@ -47,7 +50,11 @@ io.on("connection", (socket) => {
socket.on("set-log-level", (level) => {
socket.log_level = level;
createLogEvent(socket, "DEBUG", `Updated log level to ${level}`);
socket.emit("log-event", {
timestamp: new Date(),
level: "INFO",
message: `Updated log level to ${level}`,
});
});
socket.on("cdk-export-job", (jobid) => {
@@ -60,7 +67,7 @@ io.on("connection", (socket) => {
`User selected customer ID ${selectedCustomerId}`
);
socket.selectedCustomerId = selectedCustomerId;
//CdkJobExport(socket, jobid);
CdkSelectedCustomer(socket, selectedCustomerId);
});
socket.on("cdk-get-makes", async (cdk_dealerid, callback) => {
@@ -123,8 +130,40 @@ function createLogEvent(socket, level, message) {
}
}
function createXmlEvent(socket, xml, message, isError = false) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy("TRACE")) {
socket.emit("log-event", {
timestamp: new Date(),
level: isError ? "ERROR" : "TRACE",
message: `${message}: ${xml}`,
});
}
logger.log(
isError ? "ws-log-event-xml-error" : "ws-log-event-xml",
isError ? "ERROR" : "TRACE",
socket.user.email,
socket.recordid,
{
wsmessage: message,
xml,
}
);
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level: isError ? "ERROR" : "TRACE",
message,
xml,
});
}
}
function LogLevelHierarchy(level) {
switch (level) {
case "XML":
return 5;
case "TRACE":
return 5;
case "DEBUG":
@@ -141,3 +180,4 @@ function LogLevelHierarchy(level) {
}
exports.createLogEvent = createLogEvent;
exports.createXmlEvent = createXmlEvent;

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