Compare commits

...

66 Commits

Author SHA1 Message Date
Patrick Fic
77d3fc359d Resolve translation. 2021-09-13 10:09:26 -07:00
Patrick Fic
eb120a264e Add missing translations. 2021-09-13 09:49:38 -07: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
ef6cdf07d8 IO-1352 Remove positive hrs check on receivables export. 2021-09-10 13:09:21 -07:00
Patrick Fic
4059aa9875 IO-43 Updated related ROs approach. 2021-09-09 15:35:24 -07:00
Patrick Fic
e46307e715 IO-1350 Audatex import issues. 2021-09-09 13:09:05 -07: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
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
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
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
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
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
95 changed files with 32524 additions and 48746 deletions

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

@@ -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,17 +1,21 @@
import { Button, Table } from "antd";
import React, { useState } from "react";
import { Button, 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
@@ -20,6 +24,15 @@ export default connect(
export function DmsAllocationsSummary({ socket, bodyshop, jobId }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
useEffect(() => {
if (socket.connected) {
socket.emit("cdk-calculate-allocations", jobId, (ack) =>
setAllocationsSummary(ack)
);
}
}, [socket, socket.connected, jobId]);
const columns = [
{
title: t("jobs.fields.dms.center"),
@@ -71,13 +84,45 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId }) {
);
}}
>
Get
<SyncOutlined />
</Button>
)}
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>
);
}}
/>
);
}

View File

@@ -4,6 +4,9 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import { useLazyQuery } from "@apollo/client";
import { SEARCH_DMS_VEHICLES } from "../../graphql/dms.queries";
import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -12,83 +15,75 @@ 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("jobs.fields.dms.make"),
dataIndex: "make",
key: "make",
},
{
title: t("jobs.fields.dms.modelFullName"),
dataIndex: "modelFullName",
key: "modelFullName",
title: t("jobs.fields.dms.model"),
dataIndex: "model",
key: "model",
},
{
title: t("jobs.fields.dms.makeCode"),
dataIndex: "makeCode",
key: "makeCode",
title: t("jobs.fields.dms.makecode"),
dataIndex: "makecode",
key: "makecode",
},
{
title: t("jobs.fields.dms.modelCode"),
dataIndex: "modelCode",
key: "modelCode",
title: t("jobs.fields.dms.modelcode"),
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;
console.log(
"🚀 ~ file: dms-cdk-makes.component.jsx ~ line 95 ~ selectedModel",
selectedModel
);
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 +95,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.getmakes")}
</Button>
</div>
);

View File

@@ -0,0 +1,32 @@
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";
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 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}>
Refetch Models
</Button>
);
}

View File

@@ -1,10 +1,24 @@
import { Button, Table } 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,11 +29,24 @@ export default function DmsCustomerSelector() {
setcustomerList(customerList);
});
const onOk = () => {
const onUseSelected = () => {
setVisible(false);
socket.emit("cdk-selected-customer", selectedCustomer);
};
const onUseGeneric = () => {
setVisible(false);
socket.emit(
"cdk-selected-customer",
bodyshop.cdk_configuration.generic_customer_number
);
};
const onCreateNew = () => {
setVisible(false);
socket.emit("cdk-selected-customer", null);
};
const columns = [
{
title: t("dms.fields.name1"),
@@ -27,28 +54,13 @@ export default function DmsCustomerSelector() {
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"),
//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}`,
},
];
@@ -57,7 +69,23 @@ export default function DmsCustomerSelector() {
<Table
title={() => (
<div>
<Button onClick={onOk}>Select</Button>
<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" }}

View File

@@ -1,5 +1,13 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input } from "antd";
import {
Button,
Form,
Input,
InputNumber,
Select,
Space,
Statistic,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -8,7 +16,9 @@ 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 Dinero from "dinero.js";
import { determineDmsType } from "../../pages/dms/dms.container";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
@@ -17,11 +27,38 @@ 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">
<Form form={form} layout="vertical" onFinish={handleFinish}>
<LayoutFormRow>
<Form.Item
name="journal"
@@ -40,15 +77,41 @@ export function DmsPostForm({ bodyshop, socket, jobId }) {
<Input />
</Form.Item>
<Form.Item
name="dms_make"
label={t("jobs.fields.dms.dms_make")}
name="story"
label={t("jobs.fields.dms.story")}
rules={[
{
required: true,
},
]}
>
<Input />
<Input.TextArea />
</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>
<Form.Item
name="dms_make"
@@ -59,9 +122,21 @@ export function DmsPostForm({ bodyshop, socket, jobId }) {
},
]}
>
<Input />
<Input disabled />
</Form.Item>
<DmsCdkMakes form={form} socket={socket} />
<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.List name={["payers"]}>
@@ -81,20 +156,30 @@ export function DmsPostForm({ bodyshop, socket, jobId }) {
},
]}
>
<Input />
<Select
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.account")}
key={`${index}account`}
name={[field.name, "account"]}
label={t("jobs.fields.dms.payer.dms_acctnumber")}
key={`${index}dms_acctnumber`}
name={[field.name, "dms_acctnumber"]}
rules={[
{
required: true,
},
]}
>
<Input />
<Input disabled />
</Form.Item>
<Form.Item
@@ -107,8 +192,9 @@ export function DmsPostForm({ bodyshop, socket, jobId }) {
},
]}
>
<CurrencyInput />
<CurrencyInput min={0} />
</Form.Item>
<Form.Item
label={t("jobs.fields.dms.payer.controlnumber")}
key={`${index}controlnumber`}
@@ -122,6 +208,27 @@ export function DmsPostForm({ bodyshop, socket, jobId }) {
<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);
@@ -133,18 +240,69 @@ export function DmsPostForm({ bodyshop, socket, jobId }) {
<Form.Item>
<Button
type="dashed"
disabled={!(fields.length < 3)}
onClick={() => {
add();
if (fields.length < 3) add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
{t("dms.actions.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 discrep = Dinero(job.job_totals.totals.total_repairs).subtract(
totalAllocated
);
return (
<Space>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Statistic
title={t("jobs.fields.subtotal")}
value={Dinero(job.job_totals.totals.total_repairs).toFormat()}
/>
<Statistic
title={t("jobs.labels.dms.notallocated")}
valueStyle={{
color: discrep.getAmount() === 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
<Button //disabled={discrep.getAmount() !== 0} //TODO: REMOVE THIS COMMENT.
htmlType="submit"
>
{t("jobs.actions.dms.post")}
</Button>
<Button
onClick={() => {
socket.emit(`${determineDmsType(bodyshop)}-export-job`, {
jobid: job.id,
});
}}
>
Bypass
</Button>
</Space>
);
}}
</Form.Item>
</Form>
);
}

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

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

@@ -0,0 +1,19 @@
import { Space, Tag } from "antd";
import React from "react";
import { Link } from "react-router-dom";
export default function JobsRelatedRos({ jobid, job }) {
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

@@ -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,61 @@ 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
}
kmin
kmout
v_model_desc
}
}
`;
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,4 +1,4 @@
//import { useQuery } from "@apollo/client";
import { useQuery } from "@apollo/client";
import { Button, Col, Result, Row, Select, Space } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
@@ -7,14 +7,14 @@ import { connect } from "react-redux";
import { useLocation } 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,
@@ -52,10 +52,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
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");
@@ -73,7 +73,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
console.log("Connected again.");
});
socket.on("reconnect", () => {
console.log("Connected again.");
setLogs((logs) => {
return [
...logs,
@@ -102,48 +101,42 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!jobId || !bodyshop.cdk_dealerid) return <Result status="404" />;
if (!jobId || !bodyshop.cdk_dealerid || !(data && data.jobs_by_pk))
return <Result status="404" />;
const dmsType = determineDmsType(bodyshop);
// if (loading) return <LoadingSpinner />;
// if (error) return <AlertComponent message={error.message} type="error" />;
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
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}>
{data && data.jobs_by_pk && data.jobs_by_pk.ro_number}
<DmsAllocationsSummary socket={socket} jobId={jobId} />
<DmsPostForm socket={socket} jobId={jobId} />
<DmsPostForm
socket={socket}
jobId={jobId}
job={data && data.jobs_by_pk}
/>
</Col>
<Col span={6}>
<Space>
<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>
<div style={{ maxHeight: "500px", overflowY: "auto" }}>
<DmsLogEvents socket={socket} logs={logs} />
</div>
@@ -155,7 +148,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
);
}
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 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 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,13 @@
"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",
"mappingname": "DMS Mapping Name",
"srcco": "Source Company #/Dealer #"
},
"email": "General Shop Email",
"enforce_class": "Enforce Class on Conversion?",
@@ -352,6 +355,7 @@
"view": "Shift Clock -> View"
},
"shop": {
"config": "Shop -> Config",
"dashboard": "Shop -> Dashboard",
"rbac": "Shop -> RBAC",
"templates": "Shop -> Templates",
@@ -381,6 +385,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 +410,7 @@
"pap": "OEM Partial",
"par": "Recored",
"pas": "Sublet",
"pasl": "Sublet (L)",
"refund": "Refund",
"sales_tax_codes": {
"code": "Code",
@@ -474,6 +483,9 @@
"defaultprofitsmapping": "Default Profits Mapping",
"deliverchecklist": "Delivery Checklist",
"dms": {
"cdk": {
"payers": "CDK Payers"
},
"cdk_dealerid": "CDK Dealer ID",
"title": "DMS"
},
@@ -1026,7 +1038,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 +1078,16 @@
"changestatus": "Change Status",
"convert": "Convert",
"deliver": "Deliver",
"dms": {
"addpayer": "Add Payer",
"createnewcustomer": "Create New Customer",
"findmakemodelcode": "",
"getmakes": "Get Makes",
"post": "Post",
"refetchmakesmodels": "",
"usegeneric": "Use Generic Customer",
"useselected": "Use Selected Customer"
},
"dmsautoallocate": "DMS Auto Allocate",
"export": "Export",
"exportcustdata": "Export Customer Data",
@@ -1123,6 +1145,29 @@
"adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours",
"alt_transport": "Alt. Trans.",
"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": "PVRT",
"ca_customer_gst": "Customer Portion of GST",
"ca_gst_registrant": "GST Registrant",
@@ -1147,12 +1192,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": "",
"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": ""
},
"driveable": "Driveable",
"employee_body": "Body",
@@ -1373,6 +1433,14 @@
"deliverchecklist": "Deliver Checklist",
"difference": "Difference",
"diskscan": "Scan Disk for Estimates",
"dms": {
"defaultstory": "",
"kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.",
"logs": "",
"notallocated": "Not Allocated",
"postingform": "",
"totalallocated": "Total Amount Allocated"
},
"documents": "Documents",
"documents-images": "Images",
"documents-other": "Other Documents",
@@ -1420,7 +1488,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 +1504,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 +2282,7 @@
"courtesycars-detail": "Courtesy Car {{number}}",
"courtesycars-new": "New Courtesy Car",
"dashboard": "Dashboard",
"dms": "",
"export-logs": "Export Logs",
"jobs": "Jobs",
"jobs-active": "Active Jobs",
@@ -2251,6 +2321,7 @@
"courtesycars-create": "New Courtesy Car | $t(titles.app)",
"courtesycars-detail": "Courtesy Car {{id}} | $t(titles.app)",
"dashboard": "Dashboard | $t(titles.app)",
"dms": "",
"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,13 @@
"templates": ""
},
"dms": {
"cashierid": "",
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
"generic_customer_number": "",
"mappingname": "",
"srcco": ""
},
"email": "",
"enforce_class": "",
@@ -352,6 +355,7 @@
"view": ""
},
"shop": {
"config": "",
"dashboard": "",
"rbac": "",
"templates": "",
@@ -381,6 +385,10 @@
"ar": "",
"ats": "",
"federal_tax": "",
"la1": "",
"la2": "",
"la3": "",
"la4": "",
"lab": "",
"lad": "",
"lae": "",
@@ -402,6 +410,7 @@
"pap": "",
"par": "",
"pas": "",
"pasl": "",
"refund": "",
"sales_tax_codes": {
"code": "",
@@ -474,6 +483,9 @@
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk": {
"payers": ""
},
"cdk_dealerid": "",
"title": ""
},
@@ -1066,6 +1078,16 @@
"changestatus": "Cambiar Estado",
"convert": "Convertir",
"deliver": "",
"dms": {
"addpayer": "",
"createnewcustomer": "",
"findmakemodelcode": "",
"getmakes": "",
"post": "",
"refetchmakesmodels": "",
"usegeneric": "",
"useselected": ""
},
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
@@ -1123,6 +1145,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 +1192,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 +1433,14 @@
"deliverchecklist": "",
"difference": "",
"diskscan": "",
"dms": {
"defaultstory": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
"notallocated": "",
"postingform": "",
"totalallocated": ""
},
"documents": "documentos",
"documents-images": "",
"documents-other": "",
@@ -1436,6 +1504,7 @@
"removedpartsstrikethrough": ""
},
"reconciliationheader": "",
"relatedros": "",
"returntotals": "",
"rosaletotal": "",
"sale_labor": "",
@@ -2213,6 +2282,7 @@
"courtesycars-detail": "",
"courtesycars-new": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "",
"jobs-active": "",
@@ -2251,6 +2321,7 @@
"courtesycars-create": "",
"courtesycars-detail": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "Todos los trabajos | $t(titles.app)",
"jobs-admin": "",

View File

@@ -232,10 +232,13 @@
"templates": ""
},
"dms": {
"cashierid": "",
"default_journal": "",
"dms_acctnumber": "",
"dms_wip_acctnumber": "",
"mappingname": ""
"generic_customer_number": "",
"mappingname": "",
"srcco": ""
},
"email": "",
"enforce_class": "",
@@ -352,6 +355,7 @@
"view": ""
},
"shop": {
"config": "",
"dashboard": "",
"rbac": "",
"templates": "",
@@ -381,6 +385,10 @@
"ar": "",
"ats": "",
"federal_tax": "",
"la1": "",
"la2": "",
"la3": "",
"la4": "",
"lab": "",
"lad": "",
"lae": "",
@@ -402,6 +410,7 @@
"pap": "",
"par": "",
"pas": "",
"pasl": "",
"refund": "",
"sales_tax_codes": {
"code": "",
@@ -474,6 +483,9 @@
"defaultprofitsmapping": "",
"deliverchecklist": "",
"dms": {
"cdk": {
"payers": ""
},
"cdk_dealerid": "",
"title": ""
},
@@ -1066,6 +1078,16 @@
"changestatus": "Changer le statut",
"convert": "Convertir",
"deliver": "",
"dms": {
"addpayer": "",
"createnewcustomer": "",
"findmakemodelcode": "",
"getmakes": "",
"post": "",
"refetchmakesmodels": "",
"usegeneric": "",
"useselected": ""
},
"dmsautoallocate": "",
"export": "",
"exportcustdata": "",
@@ -1123,6 +1145,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 +1192,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 +1433,14 @@
"deliverchecklist": "",
"difference": "",
"diskscan": "",
"dms": {
"defaultstory": "",
"kmoutnotgreaterthankmin": "",
"logs": "",
"notallocated": "",
"postingform": "",
"totalallocated": ""
},
"documents": "Les documents",
"documents-images": "",
"documents-other": "",
@@ -1436,6 +1504,7 @@
"removedpartsstrikethrough": ""
},
"reconciliationheader": "",
"relatedros": "",
"returntotals": "",
"rosaletotal": "",
"sale_labor": "",
@@ -2213,6 +2282,7 @@
"courtesycars-detail": "",
"courtesycars-new": "",
"dashboard": "",
"dms": "",
"export-logs": "",
"jobs": "",
"jobs-active": "",
@@ -2251,6 +2321,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

9931
logs/oAuthClient-log.log Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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,79 @@ 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?`);
}
} 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,312 @@
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 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");
var QuickBooks = require("node-quickbooks");
const GraphQLClient = require("graphql-request").GraphQLClient;
const { generateOwnerTier } = require("../qbxml/qbxml-utils");
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,
});
exports.default = async (req, res) => {
try {
//Fetch the API Access Tokens & Set them for the session.
const response = await apiGqlClient.request(queries.GET_QBO_AUTH, {
email: req.user.email,
});
response.associations[0].qbo_auth;
oauthClient.setToken(response.associations[0].qbo_auth);
if (!oauthClient.token.isAccessTokenValid()) {
await refreshOauthToken(oauthClient, req);
if (!oauthClient.token.isAccessTokenValid()) {
res.sendStatus(401);
}
}
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;
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.
const jobrecord = await InsertJob(
oauthClient,
req,
job,
isThreeTier,
ownerCustomerTier
);
//Is there a job associated to the owner? If so, get the ID and move on.
//Otherwise create it.
res.sendStatus(200);
} catch (error) {
console.log(error);
res.status(400).json(error);
}
};
async function QueryInsuranceCo(oauthClient, req, job) {
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]
);
}
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 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;
}
}
// const customerCreate = {
// FullyQualifiedName: "A Test Customer",
// DisplayName: "A test Customer",
// };
// const ret = await oauthClient.makeApiCall({
// url: urlBuilder(req.cookies.qbo_realmId, "customer"),
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(customerCreate),
// });
// const invoice = {
// Line: [
// {
// DetailType: "SalesItemLineDetail",
// Amount: 100,
// SalesItemLineDetail: {
// ItemRef: {
// name: "Services",
// value: "1",
// },
// TaxCodeRef: {
// value: "2",
// },
// Qty: 1,
// UnitPrice: 100,
// },
// },
// ],
// CustomerRef: {
// name: "A test Customer",
// },
// };
// const ret2 = await oauthClient.makeApiCall({
// url: urlBuilder(req.cookies.qbo_realmId, "invoice"),
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(invoice),
// });

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

@@ -15,32 +15,101 @@ const Dinero = require("dinero.js");
const _ = require("lodash");
const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
const { performance } = require("perf_hooks");
const apiGqlClient = require("../graphql-client/graphql-client").client;
exports.default = async function (socket, cdk_dealerid) {
// 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,
"DEBUG",
`Getting makes and models list from CDK.`
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",
req.user.email,
null,
{
cdk_dealerid,
count: newList.length,
}
);
return await GetCdkMakes(socket, cdk_dealerid);
} 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,
}
);
}
};
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
);
const start = performance.now();
const soapResponseVehicleSearch =
await soapClientVehicleInsert.getMakeModelAsync(
@@ -51,28 +120,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,12 @@ exports.checkIndividualResult = checkIndividualResult;
const cdkDomain = "https://uat-3pa.dmotorworks.com";
exports.default = {
// VehicleSearch: `${cdkDomain}/pip-vehicle/services/VehicleSearch?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,6 +136,7 @@ query QUERY_JOBS_FOR_CDK_EXPORT($id: uuid!) {
ownr_fn
ownr_addr1
ownr_addr2
ownr_ph1
ownr_zip
ownr_city
ownr_st
@@ -932,6 +936,19 @@ exports.GET_AUTOHOUSE_SHOPS = `query GET_AUTOHOUSE_SHOPS {
imexshopid
}
}
`;
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 = `
@@ -1035,3 +1052,17 @@ exports.GET_CDK_ALLOCATIONS = `
}
}
`;
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
}
}
`;

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;
@@ -60,7 +63,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 +126,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 +176,4 @@ function LogLevelHierarchy(level) {
}
exports.createLogEvent = createLogEvent;
exports.createXmlEvent = createXmlEvent;

580
yarn.lock

File diff suppressed because it is too large Load Diff