Compare commits
233 Commits
feature/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bb8eee384 | ||
|
|
85d79a8d7f | ||
|
|
e73d082eab | ||
|
|
2637538d9a | ||
|
|
eb58274f90 | ||
|
|
aa410d6847 | ||
|
|
97f1be9d6f | ||
|
|
8ec5bc049b | ||
|
|
1f2bec06ef | ||
|
|
1cad57bec2 | ||
|
|
10f3c01677 | ||
|
|
fc68b669db | ||
|
|
6e22091b81 | ||
|
|
545db54c14 | ||
|
|
aa69fef9ba | ||
|
|
1e30642d28 | ||
|
|
c1dfba949e | ||
|
|
dfa9592755 | ||
|
|
8f2b1f0f78 | ||
|
|
cf91ec14c0 | ||
|
|
dfe0f99bea | ||
|
|
591022c097 | ||
|
|
c76da54b93 | ||
|
|
186f6101ff | ||
|
|
e95cf0a026 | ||
|
|
3eaa3a0189 | ||
|
|
0ba445aad2 | ||
|
|
637c718b05 | ||
|
|
a01019b1b3 | ||
|
|
f6e2393a40 | ||
|
|
715857a587 | ||
|
|
186ad2d7a2 | ||
|
|
c64c49ab6e | ||
|
|
3e067e2103 | ||
|
|
4cc1f77b14 | ||
|
|
334dd5fe94 | ||
|
|
d73f37cacc | ||
|
|
c6a7e3dcb1 | ||
|
|
4bdc02d22c | ||
|
|
afa8e04008 | ||
|
|
990c1bb2bf | ||
|
|
a5a59c526c | ||
|
|
2d5bef4b7b | ||
|
|
5893b1e3b3 | ||
|
|
0af452b8a4 | ||
|
|
3d82198c90 | ||
|
|
01bca360c7 | ||
|
|
59e994ac29 | ||
|
|
d5f3105341 | ||
|
|
92920f69d4 | ||
|
|
61ac520192 | ||
|
|
45176cc2e2 | ||
|
|
6aacce5a58 | ||
|
|
7a515c35d2 | ||
|
|
80dc04669c | ||
|
|
a702c87986 | ||
|
|
1942103985 | ||
|
|
fd161fa9eb | ||
|
|
44a18f4ace | ||
|
|
3269c4f602 | ||
|
|
43cbf0084a | ||
|
|
28b1356f76 | ||
|
|
38456cb213 | ||
|
|
8c826eaaed | ||
|
|
f047556ab5 | ||
|
|
057b335e82 | ||
|
|
30c7da2bf9 | ||
|
|
82fb4fc74c | ||
|
|
151f122b8c | ||
|
|
b198ca5051 | ||
|
|
d9abe84949 | ||
|
|
17f4d69e30 | ||
|
|
77d3fc359d | ||
|
|
86151f3337 | ||
|
|
ae4b5bca33 | ||
|
|
eb120a264e | ||
|
|
7e3f496ea1 | ||
|
|
4d33f16f13 | ||
|
|
d7ebefe7ab | ||
|
|
8dc2197677 | ||
|
|
fe993cba73 | ||
|
|
ef6cdf07d8 | ||
|
|
cdbde6f5fa | ||
|
|
4059aa9875 | ||
|
|
22e30ae5cb | ||
|
|
78c883743f | ||
|
|
e46307e715 | ||
|
|
f3e078b481 | ||
|
|
c03f54b3eb | ||
|
|
bb0234fb7d | ||
|
|
03f25c8c0e | ||
|
|
8e5005daa0 | ||
|
|
259458eec3 | ||
|
|
59e57aa274 | ||
|
|
de33bcd72b | ||
|
|
d72472ccc3 | ||
|
|
91efd170c8 | ||
|
|
6f1ddd51fd | ||
|
|
9f48a91a29 | ||
|
|
a7e2548e14 | ||
|
|
3294faaeaa | ||
|
|
06480159e3 | ||
|
|
6ce06ed5c0 | ||
|
|
71e535388a | ||
|
|
dbd265d368 | ||
|
|
c11f182f83 | ||
|
|
5f70cfd585 | ||
|
|
6e0675f28b | ||
|
|
96f45d2c80 | ||
|
|
091e44f471 | ||
|
|
00549d6a88 | ||
|
|
d940e0ee78 | ||
|
|
39a38d46ee | ||
|
|
71435ed75a | ||
|
|
e7ec408b98 | ||
|
|
9306064420 | ||
|
|
37a16edfb4 | ||
|
|
9ad5b9547f | ||
|
|
47edb0bdf4 | ||
|
|
5db47e879c | ||
|
|
54b46dd25e | ||
|
|
4cb92c8508 | ||
|
|
cb76e2dcde | ||
|
|
901c64ed85 | ||
|
|
f6f90d68fa | ||
|
|
05e295fcac | ||
|
|
c97df6dc61 | ||
|
|
61406aafa6 | ||
|
|
03210db711 | ||
|
|
b3a34c109a | ||
|
|
81daad35d8 | ||
|
|
529eb24d76 | ||
|
|
e81bf7b561 | ||
|
|
7a35dc9b38 | ||
|
|
c72ef97b82 | ||
|
|
5284ee2ef9 | ||
|
|
724c097d52 | ||
|
|
3c3da178ba | ||
|
|
db4e5d48af | ||
|
|
a7cf081ed5 | ||
|
|
db5b11f6d3 | ||
|
|
8d3d52485f | ||
|
|
edb58ebc81 | ||
|
|
971b518e8f | ||
|
|
8d9507dce1 | ||
|
|
748f8f472d | ||
|
|
db2b4739c2 | ||
|
|
6c12e5cb03 | ||
|
|
140e57a123 | ||
|
|
3ca6791939 | ||
|
|
1c473c95a2 | ||
|
|
7e145bdec7 | ||
|
|
44c17bd7a2 | ||
|
|
c493f6e31e | ||
|
|
5213b0b315 | ||
|
|
8bfd0a1c16 | ||
|
|
0541167cc5 | ||
|
|
834966ae96 | ||
|
|
e7d813c3f3 | ||
|
|
26e47ff203 | ||
|
|
d2b2a5399d | ||
|
|
5a4d6d3e8c | ||
|
|
61a5e180f4 | ||
|
|
d5b8ea3ac5 | ||
|
|
4e87ef179b | ||
|
|
3b7c31626d | ||
|
|
a57e35354d | ||
|
|
a269fd3ad8 | ||
|
|
86bee9ad0d | ||
|
|
f8b57ca9bd | ||
|
|
5f7b780195 | ||
|
|
ba5400d04a | ||
|
|
0190e737c1 | ||
|
|
db64d9b69f | ||
|
|
0d12ab36a9 | ||
|
|
8f52efbaca | ||
|
|
d6dc5db185 | ||
|
|
58d1859640 | ||
|
|
dba648aea2 | ||
|
|
12f6206f88 | ||
|
|
7d9fd06b6d | ||
|
|
d6fbf16272 | ||
|
|
b68de683b0 | ||
|
|
2f9d025fbe | ||
|
|
9c1ffaba17 | ||
|
|
142df39cd2 | ||
|
|
ef79ccc299 | ||
|
|
6d2d96be5c | ||
|
|
8ff2a6e6c4 | ||
|
|
b793aa3394 | ||
|
|
be2cfb908a | ||
|
|
e34084b146 | ||
|
|
c1d7168260 | ||
|
|
b3a3709b72 | ||
|
|
dd59f3d026 | ||
|
|
ccb00ff391 | ||
|
|
b37970a6df | ||
|
|
2cd7fcfbd8 | ||
|
|
77819b06fe | ||
|
|
4e936b4cff | ||
|
|
b2362a85fa | ||
|
|
f45f351678 | ||
|
|
311171fa0a | ||
|
|
7e05f03f4d | ||
|
|
76826c1e80 | ||
|
|
18618efdc0 | ||
|
|
d1952dfc25 | ||
|
|
fe5f4f2727 | ||
|
|
fd7c907b8f | ||
|
|
0273255c2c | ||
|
|
e86160e530 | ||
|
|
9f7e0d611a | ||
|
|
0b523efa95 | ||
|
|
6b64499e24 | ||
|
|
e78e628bec | ||
|
|
ed8eb51c2f | ||
|
|
124d68ef68 | ||
|
|
3e802c1465 | ||
|
|
ac18e78897 | ||
|
|
fed16a4aa3 | ||
|
|
7ac1fa5abf | ||
|
|
c4fdef445e | ||
|
|
11cfef904b | ||
|
|
c9ed8a9360 | ||
|
|
7999895323 | ||
|
|
71ef3dadc5 | ||
|
|
f0d6c5e1b1 | ||
|
|
991df9c48f | ||
|
|
84b39f3d2b | ||
|
|
4ab0947cc8 | ||
|
|
8cbef14ea3 | ||
|
|
105ecd4221 | ||
|
|
3176cfcc56 |
17689
_reference/CDK Testing Accounts
Normal file
6
_reference/Responsibility Center Setup.md
Normal file
@@ -1,9 +1,25 @@
|
||||
// craco.config.js
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const CracoLessPlugin = require("craco-less");
|
||||
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
plugin: SentryWebpackPlugin,
|
||||
options: {
|
||||
// sentry-cli configuration
|
||||
authToken:
|
||||
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
||||
org: "snapt-software",
|
||||
project: "imexonline",
|
||||
release: process.env.REACT_APP_GIT_SHA,
|
||||
|
||||
// webpack-specific configuration
|
||||
include: ".",
|
||||
ignore: ["node_modules", "webpack.config.js"],
|
||||
},
|
||||
},
|
||||
{
|
||||
plugin: CracoLessPlugin,
|
||||
options: {
|
||||
@@ -53,4 +69,5 @@ module.exports = {
|
||||
},
|
||||
}),
|
||||
},
|
||||
devtool: "source-map",
|
||||
};
|
||||
|
||||
@@ -4,81 +4,87 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:5000",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.21",
|
||||
"@craco/craco": "^6.2.0",
|
||||
"@fingerprintjs/fingerprintjs": "^3.2.0",
|
||||
"@apollo/client": "^3.4.14",
|
||||
"@craco/craco": "^6.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^3.3.0",
|
||||
"@lourenci/react-kanban": "^2.1.0",
|
||||
"@sentry/react": "^6.10.0",
|
||||
"@sentry/tracing": "^6.10.0",
|
||||
"@stripe/react-stripe-js": "^1.4.0",
|
||||
"@stripe/stripe-js": "^1.16.0",
|
||||
"@tanem/react-nprogress": "^3.0.74",
|
||||
"antd": "^4.16.8",
|
||||
"@openreplay/tracker": "^3.4.0",
|
||||
"@openreplay/tracker-assist": "^3.4.0",
|
||||
"@openreplay/tracker-graphql": "^3.0.0",
|
||||
"@openreplay/tracker-redux": "^3.0.0",
|
||||
"@sentry/react": "^6.13.2",
|
||||
"@sentry/tracing": "^6.13.2",
|
||||
"@stripe/react-stripe-js": "^1.5.0",
|
||||
"@stripe/stripe-js": "^1.18.0",
|
||||
"@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.1.0",
|
||||
"graphql": "^15.6.0",
|
||||
"i18next": "^21.1.1",
|
||||
"i18next-browser-languagedetector": "^6.1.2",
|
||||
"jsoneditor": "^9.5.2",
|
||||
"jsoneditor": "^9.5.6",
|
||||
"jsreport-browser-client-dist": "^1.3.0",
|
||||
"libphonenumber-js": "^1.9.22",
|
||||
"logrocket": "^1.2.0",
|
||||
"markerjs2": "^2.9.0",
|
||||
"libphonenumber-js": "^1.9.34",
|
||||
"logrocket": "^2.0.0",
|
||||
"markerjs2": "^2.12.0",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"phone": "^3.1.2",
|
||||
"phone": "^3.1.8",
|
||||
"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.12.0",
|
||||
"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.4",
|
||||
"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.42.1",
|
||||
"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.3.0",
|
||||
"workbox-broadcast-update": "^6.3.0",
|
||||
"workbox-cacheable-response": "^6.3.0",
|
||||
"workbox-core": "^6.3.0",
|
||||
"workbox-expiration": "^6.3.0",
|
||||
"workbox-google-analytics": "^6.3.0",
|
||||
"workbox-navigation-preload": "^6.3.0",
|
||||
"workbox-precaching": "^6.3.0",
|
||||
"workbox-range-requests": "^6.3.0",
|
||||
"workbox-routing": "^6.3.0",
|
||||
"workbox-strategies": "^6.3.0",
|
||||
"workbox-streams": "^6.3.0"
|
||||
},
|
||||
"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",
|
||||
@@ -108,6 +114,8 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/webpack-plugin": "^1.17.1",
|
||||
"patch-package": "^6.4.7",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
}
|
||||
|
||||
13087
client/patches/peerjs+1.3.2.patch
Normal file
@@ -8,8 +8,32 @@ import { useTranslation } from "react-i18next";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import trackerGraphQL from "@openreplay/tracker-graphql";
|
||||
//import trackerRedux from "@openreplay/tracker-redux";
|
||||
import Tracker from "@openreplay/tracker";
|
||||
//import trackerAssist from "@openreplay/tracker-assist";
|
||||
import { getCurrentUser } from "../firebase/firebase.utils";
|
||||
moment.locale("en-US");
|
||||
|
||||
export const tracker = new Tracker({
|
||||
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: async ({ sessionID }) => {
|
||||
const user = await getCurrentUser();
|
||||
tracker.setUserID(user.email);
|
||||
console.log("ORS SESSION ", sessionID, user.email);
|
||||
},
|
||||
});
|
||||
|
||||
// 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");
|
||||
|
||||
export default function AppContainer() {
|
||||
|
||||
24
client/src/assets/C2QB_composite_English.svg
Normal file
|
After Width: | Height: | Size: 51 KiB |
24
client/src/assets/C2QB_transparent_English.svg
Normal file
|
After Width: | Height: | Size: 51 KiB |
4
client/src/assets/qbo/C2QB_green_btn_med_default.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
5
client/src/assets/qbo/C2QB_green_btn_med_hover.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
4
client/src/assets/qbo/C2QB_green_btn_short_default.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
5
client/src/assets/qbo/C2QB_green_btn_short_hover.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
4
client/src/assets/qbo/C2QB_green_btn_tall_default.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
5
client/src/assets/qbo/C2QB_green_btn_tall_hover.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
5
client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,25 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import queryString from "query-string";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
export default function AccountingPayablesTableComponent({ loading, bills }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AccountingPayablesTableComponent);
|
||||
|
||||
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedBills, setSelectedBills] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
@@ -109,6 +126,17 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
||||
<Checkbox disabled checked={record.is_credit_memo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => {
|
||||
const success = record.exportlogs.filter((e) => e.successful).length;
|
||||
const attempts = record.exportlogs.length;
|
||||
return `${success}/${attempts}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
@@ -121,6 +149,7 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
||||
billId={record.id}
|
||||
disabled={transInProgress || !!record.exported}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedBills={setSelectedBills}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -154,6 +183,9 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedBills}
|
||||
/>
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||
<QboAuthorizeComponent />
|
||||
)}
|
||||
<Input
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
|
||||
@@ -108,7 +108,17 @@ export default function AccountingPayablesTableComponent({
|
||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => {
|
||||
const success = record.exportlogs.filter((e) => e.successful).length;
|
||||
const attempts = record.exportlogs.length;
|
||||
return `${success}/${attempts}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
@@ -120,6 +130,7 @@ export default function AccountingPayablesTableComponent({
|
||||
paymentId={record.id}
|
||||
disabled={transInProgress || !!record.exportedat}
|
||||
loadingCallback={setTransInProgress}
|
||||
setSelectedPayments={setSelectedPayments}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -8,7 +8,26 @@ import { alphaSort } from "../../utils/sorters";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
|
||||
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(AccountingReceivablesTableComponent);
|
||||
|
||||
export function AccountingReceivablesTableComponent({
|
||||
bodyshop,
|
||||
loading,
|
||||
jobs,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
@@ -114,17 +133,28 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("exportlogs.labels.attempts"),
|
||||
dataIndex: "attempts",
|
||||
key: "attempts",
|
||||
|
||||
render: (text, record) => {
|
||||
const success = record.exportlogs.filter((e) => e.successful).length;
|
||||
const attempts = record.exportlogs.length;
|
||||
return `${success}/${attempts}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<JobExportButton
|
||||
jobId={record.id}
|
||||
disabled={!!record.date_exported}
|
||||
setSelectedJobs={setSelectedJobs}
|
||||
/>
|
||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||
@@ -169,12 +199,17 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
/>
|
||||
{!bodyshop.cdk_dealerid && (
|
||||
<JobsExportAllButton
|
||||
jobIds={selectedJobs}
|
||||
disabled={transInProgress || selectedJobs.length === 0}
|
||||
loadingCallback={setTransInProgress}
|
||||
completedCallback={setSelectedJobs}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||
<QboAuthorizeComponent />
|
||||
)}
|
||||
<Input.Search
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
|
||||
@@ -18,8 +18,10 @@ import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -48,7 +50,31 @@ function BillEnterModalContainer({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const client = useApolloClient();
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
return {
|
||||
...billEnterModal.context.bill,
|
||||
jobid:
|
||||
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
||||
federal_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
|
||||
0,
|
||||
state_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
|
||||
0,
|
||||
local_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
|
||||
0,
|
||||
};
|
||||
}, [billEnterModal, bodyshop]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
let totals = CalculateBillTotal(values);
|
||||
if (totals.discrepancy.getAmount() !== 0) {
|
||||
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const { upload, location, ...remainingValues } = values;
|
||||
|
||||
@@ -190,7 +216,7 @@ function BillEnterModalContainer({
|
||||
|
||||
if (enterAgain) {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ billlines: [] });
|
||||
form.setFieldsValue(formValues);
|
||||
} else {
|
||||
toggleModalVisible();
|
||||
}
|
||||
@@ -208,23 +234,6 @@ function BillEnterModalContainer({
|
||||
if (enterAgain) form.submit();
|
||||
}, [enterAgain, form]);
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
return {
|
||||
...billEnterModal.context.bill,
|
||||
jobid:
|
||||
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
||||
federal_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
|
||||
0,
|
||||
state_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
|
||||
0,
|
||||
local_tax_rate:
|
||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
|
||||
0,
|
||||
};
|
||||
}, [billEnterModal, bodyshop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (billEnterModal.visible) {
|
||||
form.setFieldsValue(formValues);
|
||||
|
||||
@@ -72,9 +72,11 @@ export function BillEnterModalLinesComponent({
|
||||
quantity: opt.part_qty || 1,
|
||||
actual_price: opt.cost,
|
||||
cost_center: opt.part_type
|
||||
? responsibilityCenters.defaults.costs[
|
||||
? responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[
|
||||
opt.part_type
|
||||
] || null
|
||||
] ||
|
||||
null)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
export const CalculateBillTotal = (invoice) => {
|
||||
const {
|
||||
total,
|
||||
billlines,
|
||||
federal_tax_rate,
|
||||
local_tax_rate,
|
||||
state_tax_rate,
|
||||
} = invoice;
|
||||
const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } =
|
||||
invoice;
|
||||
|
||||
//TODO Determine why this recalculates so many times.
|
||||
let subtotal = Dinero({ amount: 0 });
|
||||
@@ -20,8 +15,7 @@ export const CalculateBillTotal = (invoice) => {
|
||||
billlines.forEach((i) => {
|
||||
if (!!i) {
|
||||
const itemTotal = Dinero({
|
||||
amount:
|
||||
Math.round(((i.actual_cost || 0) * 100 + Number.EPSILON) * 100) / 100,
|
||||
amount: Math.round((i.actual_cost || 0) * 100),
|
||||
}).multiply(i.quantity || 1);
|
||||
|
||||
subtotal = subtotal.add(itemTotal);
|
||||
|
||||
@@ -13,6 +13,7 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
|
||||
ref={ref}
|
||||
showSearch
|
||||
optionFilterProp="line_desc"
|
||||
notFoundContent={"Removed."}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
@@ -21,14 +22,18 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
|
||||
{options
|
||||
? options.map((item) => (
|
||||
<Option
|
||||
disabled={item.removed}
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
cost={item.act_price ? item.act_price : 0}
|
||||
part_type={item.part_type}
|
||||
line_desc={item.line_desc}
|
||||
part_qty={item.part_qty}
|
||||
style={{
|
||||
...(item.removed ? { textDecoration: "line-through" } : {}),
|
||||
}}
|
||||
>
|
||||
{`${item.line_desc}${
|
||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}`}
|
||||
</Option>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { selectSelectedConversation } from "../../redux/messaging/messaging.sele
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation,
|
||||
@@ -60,13 +61,18 @@ export function ChatConversationListComponent({
|
||||
) : (
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
)}
|
||||
{item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx} className="ro-number-tag">
|
||||
{j.job.ro_number}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
<div sryle={{ display: "inline-block" }}>
|
||||
<div>
|
||||
{item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx} className="ro-number-tag">
|
||||
{j.job.ro_number}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
||||
</div>
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Icon from "@ant-design/icons";
|
||||
import { Tooltip } from "antd";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
CellMeasurerCache,
|
||||
List,
|
||||
} from "react-virtualized";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import "./chat-message-list.styles.scss";
|
||||
|
||||
export default function ChatMessageListComponent({ messages }) {
|
||||
@@ -85,17 +87,22 @@ export default function ChatMessageListComponent({ messages }) {
|
||||
|
||||
const MessageRender = (message) => {
|
||||
return (
|
||||
<div>
|
||||
{message.image_path &&
|
||||
message.image_path.map((i, idx) => (
|
||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||
<a href={i} target="__blank">
|
||||
<img alt="Received" className="message-img" src={i} />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<div>{message.text}</div>
|
||||
</div>
|
||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||
<div>
|
||||
{message.image_path &&
|
||||
message.image_path.map((i, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
style={{ display: "flex", justifyContent: "center" }}
|
||||
>
|
||||
<a href={i} target="__blank">
|
||||
<img alt="Received" className="message-img" src={i} />
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
<div>{message.text}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -83,8 +83,10 @@ export function ContractsList({
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||
record.courtesycar.year
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model} ${
|
||||
record.courtesycar.plate ? `(${record.courtesycar.plate})` : ""
|
||||
} ${record.courtesycar.make} ${record.courtesycar.model}${
|
||||
record.courtesycar.plate ? ` (${record.courtesycar.plate})` : ""
|
||||
}${
|
||||
record.courtesycar.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""
|
||||
}`}</Link>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -31,6 +31,9 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
<Option value="courtesycars.status.out">
|
||||
{t("courtesycars.status.out")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.sold">
|
||||
{t("courtesycars.status.sold")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
text: t("courtesycars.status.out"),
|
||||
value: "courtesycars.status.out",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.sold"),
|
||||
value: "courtesycars.status.sold",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
sortOrder:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Button, Card, Table, Typography } from "antd";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import Dinero from "dinero.js";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DmsAllocationsSummary);
|
||||
|
||||
export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
const { t } = useTranslation();
|
||||
const [allocationsSummary, setAllocationsSummary] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket.connected) {
|
||||
socket.emit("cdk-calculate-allocations", jobId, (ack) => {
|
||||
setAllocationsSummary(ack);
|
||||
socket.allocationsSummary = ack;
|
||||
});
|
||||
}
|
||||
}, [socket, socket.connected, jobId]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.dms.center"),
|
||||
dataIndex: "center",
|
||||
key: "center",
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.sale"),
|
||||
dataIndex: "sale",
|
||||
key: "sale",
|
||||
render: (text, record) => Dinero(record.sale).toFormat(),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
render: (text, record) => Dinero(record.cost).toFormat(),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.sale_dms_acctnumber"),
|
||||
dataIndex: "sale_dms_acctnumber",
|
||||
key: "sale_dms_acctnumber",
|
||||
render: (text, record) =>
|
||||
record.profitCenter && record.profitCenter.dms_acctnumber,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.cost_dms_acctnumber"),
|
||||
dataIndex: "cost_dms_acctnumber",
|
||||
key: "cost_dms_acctnumber",
|
||||
render: (text, record) =>
|
||||
record.costCenter && record.costCenter.dms_acctnumber,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.dms_wip_acctnumber"),
|
||||
dataIndex: "dms_wip_acctnumber",
|
||||
key: "dms_wip_acctnumber",
|
||||
render: (text, record) =>
|
||||
record.costCenter && record.costCenter.dms_wip_acctnumber,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
extra={
|
||||
<Button
|
||||
onClick={() => {
|
||||
socket.emit("cdk-calculate-allocations", jobId, (ack) =>
|
||||
setAllocationsSummary(ack)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
columns={columns}
|
||||
rowKey="center"
|
||||
dataSource={allocationsSummary}
|
||||
summary={() => {
|
||||
const totals = allocationsSummary.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost)),
|
||||
};
|
||||
},
|
||||
{
|
||||
totalSale: Dinero(),
|
||||
totalCost: Dinero(),
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.totals")}
|
||||
</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{totals.totalSale.toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{
|
||||
// totals.totalCost.toFormat()
|
||||
}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell></Table.Summary.Cell>
|
||||
<Table.Summary.Cell></Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
105
client/src/components/dms-cdk-makes/dms-cdk-makes.component.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Button, Input, Modal, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { SEARCH_DMS_VEHICLES } from "../../graphql/dms.queries";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles);
|
||||
|
||||
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("vehicles.fields.v_make_desc"),
|
||||
dataIndex: "make",
|
||||
key: "make",
|
||||
},
|
||||
{
|
||||
title: t("vehicles.fields.v_model_desc"),
|
||||
dataIndex: "model",
|
||||
key: "model",
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.dms_make"),
|
||||
dataIndex: "makecode",
|
||||
key: "makecode",
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.dms_model"),
|
||||
dataIndex: "modelcode",
|
||||
key: "modelcode",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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) => callSearch({ variables: { search: val } })}
|
||||
placeholder={t("general.labels.search")}
|
||||
/>
|
||||
)}
|
||||
columns={columns}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
dataSource={data ? data.search_dms_vehicles : []}
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: () => setSelectedModel(record),
|
||||
};
|
||||
}}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
setSelectedModel(record);
|
||||
},
|
||||
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedModel && selectedModel.id],
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
callSearch({
|
||||
variables: {
|
||||
search: job && job.v_model_desc && job.v_model_desc.substr(0, 3),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.dms.findmakemodelcode")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Button } from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakesRefetch);
|
||||
|
||||
export function DmsCdkMakesRefetch({ bodyshop, form, socket }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const handleRefetch = async () => {
|
||||
setLoading(true);
|
||||
const response = await axios.post("/cdk/getvehicles", {
|
||||
cdk_dealerid: bodyshop.cdk_dealerid,
|
||||
bodyshopid: bodyshop.id,
|
||||
});
|
||||
console.log(response);
|
||||
setLoading(false);
|
||||
};
|
||||
return (
|
||||
<Button loading={loading} onClick={handleRefetch}>
|
||||
{t("jobs.actions.dms.refetchmakesmodels")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Button, Table, Col , Checkbox} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { socket } from "../../pages/dms/dms.container";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
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);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
|
||||
socket.on("cdk-select-customer", (customerList, callback) => {
|
||||
setVisible(true);
|
||||
setcustomerList(customerList);
|
||||
});
|
||||
|
||||
const onUseSelected = () => {
|
||||
setVisible(false);
|
||||
socket.emit("cdk-selected-customer", selectedCustomer);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onUseGeneric = () => {
|
||||
setVisible(false);
|
||||
socket.emit(
|
||||
"cdk-selected-customer",
|
||||
bodyshop.cdk_configuration.generic_customer_number
|
||||
);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
setVisible(false);
|
||||
socket.emit("cdk-selected-customer", null);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.dms.id"),
|
||||
dataIndex: ["id", "value"],
|
||||
key: "id",
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.vinowner"),
|
||||
dataIndex: "vinOwner",
|
||||
key: "vinOwner",
|
||||
render: (text, record) => <Checkbox disabled checked={record.vinOwner}/>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
dataIndex: ["name1", "fullName"],
|
||||
key: "name1",
|
||||
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
//dataIndex: ["name2", "fullName"],
|
||||
key: "address",
|
||||
render: (record, value) =>
|
||||
`${record?.address?.addressLine[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`,
|
||||
},
|
||||
];
|
||||
|
||||
if (!visible) return <></>;
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Table
|
||||
title={() => (
|
||||
<div>
|
||||
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
|
||||
{t("jobs.actions.dms.useselected")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onUseGeneric}
|
||||
disabled={
|
||||
!(
|
||||
bodyshop.cdk_configuration &&
|
||||
bodyshop.cdk_configuration.generic_customer_number
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("jobs.actions.dms.usegeneric")}
|
||||
</Button>
|
||||
<Button onClick={onCreateNew}>
|
||||
{t("jobs.actions.dms.createnewcustomer")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.id.value}
|
||||
dataSource={customerList}
|
||||
//onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (props) => {
|
||||
setSelectedCustomer(props.id.value);
|
||||
},
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedCustomer],
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Divider, Space, Tag, Timeline } from "antd";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
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 default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
|
||||
|
||||
export function DmsLogEvents({ socket, logs, bodyshop }) {
|
||||
return (
|
||||
<Timeline pending reverse={true}>
|
||||
{logs.map((log, idx) => (
|
||||
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
|
||||
<Space wrap align="start" style={{}}>
|
||||
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
|
||||
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:MM:ss")}</span>
|
||||
<Divider type="vertical" />
|
||||
<span>{log.message}</span>
|
||||
</Space>
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
);
|
||||
}
|
||||
|
||||
function LogLevelHierarchy(level) {
|
||||
switch (level) {
|
||||
case "TRACE":
|
||||
return "pink";
|
||||
case "DEBUG":
|
||||
return "orange";
|
||||
case "INFO":
|
||||
return "blue";
|
||||
case "WARNING":
|
||||
return "yellow";
|
||||
case "ERROR":
|
||||
return "red";
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
340
client/src/components/dms-post-form/dms-post-form.component.jsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { determineDmsType } from "../../pages/dms/dms.container";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||
|
||||
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 (
|
||||
<Card title={t("jobs.labels.dms.postingform")}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
initialValues={{
|
||||
story: t("jobs.labels.dms.defaultstory", {
|
||||
ro_number: job.ro_number,
|
||||
area_of_damage: job.area_of_damage && job.area_of_damage.impact1,
|
||||
}).substr(0, 239),
|
||||
}}
|
||||
>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item
|
||||
name="journal"
|
||||
label={t("jobs.fields.dms.journal")}
|
||||
initialValue={
|
||||
bodyshop.cdk_configuration &&
|
||||
bodyshop.cdk_configuration.default_journal
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="kmin"
|
||||
label={t("jobs.fields.kmin")}
|
||||
initialValue={job && job.kmin}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="kmout"
|
||||
label={t("jobs.fields.kmout")}
|
||||
initialValue={job && job.kmout}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber disabled />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow style={{ justifyContent: "center" }} grow>
|
||||
<Form.Item
|
||||
name="dms_make"
|
||||
label={t("jobs.fields.dms.dms_make")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="dms_model"
|
||||
label={t("jobs.fields.dms.dms_model")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<DmsCdkMakes form={form} socket={socket} job={job} />
|
||||
<DmsCdkMakesRefetch />
|
||||
</LayoutFormRow>
|
||||
<Form.Item
|
||||
name="story"
|
||||
label={t("jobs.fields.dms.story")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input.TextArea maxLength={240} />
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Form.List name={["payers"]}>
|
||||
{(fields, { add, remove }) => {
|
||||
return (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<Space wrap>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.name")}
|
||||
key={`${index}name`}
|
||||
name={[field.name, "name"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
style={{ minWidth: "15rem" }}
|
||||
onSelect={(value) => handlePayerSelect(value, index)}
|
||||
>
|
||||
{bodyshop.cdk_configuration &&
|
||||
bodyshop.cdk_configuration.payers &&
|
||||
bodyshop.cdk_configuration.payers.map((payer) => (
|
||||
<Select.Option key={payer.name}>
|
||||
{payer.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.dms_acctnumber")}
|
||||
key={`${index}dms_acctnumber`}
|
||||
name={[field.name, "dms_acctnumber"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.amount")}
|
||||
key={`${index}amount`}
|
||||
name={[field.name, "amount"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.controlnumber")}
|
||||
key={`${index}controlnumber`}
|
||||
name={[field.name, "controlnumber"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const payers = form.getFieldValue("payers");
|
||||
|
||||
const row = payers && payers[index];
|
||||
|
||||
const cdkPayer =
|
||||
bodyshop.cdk_configuration.payers &&
|
||||
bodyshop.cdk_configuration.payers.find(
|
||||
(i) => i && row && i.name === row.name
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{cdkPayer &&
|
||||
t(`jobs.fields.${cdkPayer.control_type}`)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<DeleteFilled
|
||||
onClick={() => {
|
||||
remove(field.name);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
disabled={!(fields.length < 3)}
|
||||
onClick={() => {
|
||||
if (fields.length < 3) add();
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
{t("jobs.actions.dms.addpayer")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
//Perform Calculation to determine discrepancy.
|
||||
let totalAllocated = Dinero();
|
||||
|
||||
const payers = form.getFieldValue("payers");
|
||||
payers &&
|
||||
payers.forEach((payer) => {
|
||||
totalAllocated = totalAllocated.add(
|
||||
Dinero({ amount: Math.round((payer?.amount || 0) * 100) })
|
||||
);
|
||||
});
|
||||
|
||||
const totals =
|
||||
socket.allocationsSummary &&
|
||||
socket.allocationsSummary.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost)),
|
||||
};
|
||||
},
|
||||
{
|
||||
totalSale: Dinero(),
|
||||
totalCost: Dinero(),
|
||||
}
|
||||
);
|
||||
const discrep = totals
|
||||
? totals.totalSale.subtract(totalAllocated)
|
||||
: Dinero();
|
||||
return (
|
||||
<Space size="large" wrap align="center">
|
||||
<Statistic
|
||||
title={t("jobs.labels.subtotal")}
|
||||
value={(totals ? totals.totalSale : Dinero()).toFormat()}
|
||||
/>
|
||||
<Typography.Title>-</Typography.Title>
|
||||
<Statistic
|
||||
title={t("jobs.labels.dms.totalallocated")}
|
||||
value={totalAllocated.toFormat()}
|
||||
/>
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Statistic
|
||||
title={t("jobs.labels.dms.notallocated")}
|
||||
valueStyle={{
|
||||
color: discrep.getAmount() === 0 ? "green" : "red",
|
||||
}}
|
||||
value={discrep.toFormat()}
|
||||
/>
|
||||
<Button
|
||||
disabled={
|
||||
!socket.allocationsSummary || discrep.getAmount() !== 0
|
||||
}
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("jobs.actions.dms.post")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { tracker } from "../../App/App.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -36,6 +37,7 @@ class ErrorBoundary extends React.Component {
|
||||
componentDidCatch(error, info) {
|
||||
console.log("Exception Caught by Error Boundary.", error, info);
|
||||
this.setState({ ...this.state, error, info });
|
||||
tracker.event("error_boundary", error, true);
|
||||
}
|
||||
|
||||
handleErrorSubmit = () => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -168,7 +168,6 @@ export default function GlobalSearch() {
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
allowClear
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
>
|
||||
<Input.Search loading={loading} />
|
||||
|
||||
@@ -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" && {
|
||||
@@ -117,21 +119,22 @@ export function JobChecklistForm({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//Updae Owner Allow to Text
|
||||
const updateOwnerResult = await updateOwner({
|
||||
variables: {
|
||||
ownerId: job.owner.id,
|
||||
owner: { allow_text_message: values.allow_text_message },
|
||||
},
|
||||
});
|
||||
|
||||
if (!!updateOwnerResult.errors) {
|
||||
notification["error"]({
|
||||
message: t("checklist.errors.complete", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
if (type === "intake" && job.owner && job.owner.id) {
|
||||
//Updae Owner Allow to Text
|
||||
const updateOwnerResult = await updateOwner({
|
||||
variables: {
|
||||
ownerId: job.owner.id,
|
||||
owner: { allow_text_message: values.allow_text_message },
|
||||
},
|
||||
});
|
||||
|
||||
if (!!updateOwnerResult.errors) {
|
||||
notification["error"]({
|
||||
message: t("checklist.errors.complete", {
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -176,7 +179,7 @@ export function JobChecklistForm({
|
||||
initialValues={{
|
||||
...(type === "intake" && {
|
||||
addToProduction: true,
|
||||
allow_text_message: job.owner.allow_text_message,
|
||||
allow_text_message: job.owner && job.owner.allow_text_message,
|
||||
scheduled_completion:
|
||||
(job && job.scheduled_completion) ||
|
||||
moment().businessAdd(
|
||||
@@ -244,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>
|
||||
|
||||
@@ -400,7 +400,7 @@ export function JobLinesComponent({
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: {
|
||||
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL"],
|
||||
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"],
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -435,7 +435,7 @@ export function JobLinesComponent({
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||
pagination={false}
|
||||
dataSource={jobLines}
|
||||
onChange={handleTableChange}
|
||||
scroll={{
|
||||
|
||||
@@ -72,7 +72,7 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
|
||||
style={{ width: "100%", minHeight: "1rem", cursor: "pointer" }}
|
||||
onClick={() => !disabled && setEditing(true)}
|
||||
>
|
||||
{jobline.status}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -21,7 +21,8 @@ export default function JobReconciliationBillsTable({
|
||||
title: t("billlines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
width: "35%",
|
||||
ellipsis: true,
|
||||
minWidth: "65rem",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
@@ -30,7 +31,7 @@ export default function JobReconciliationBillsTable({
|
||||
title: t("billlines.labels.from"),
|
||||
dataIndex: "from",
|
||||
key: "from",
|
||||
width: "20%",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) =>
|
||||
`${record.bill.vendor && record.bill.vendor.name} / ${
|
||||
@@ -42,6 +43,7 @@ export default function JobReconciliationBillsTable({
|
||||
dataIndex: "actual_price",
|
||||
key: "actual_price",
|
||||
sorter: (a, b) => a.actual_price - b.actual_price,
|
||||
width: "7rem",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_price" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
@@ -53,6 +55,7 @@ export default function JobReconciliationBillsTable({
|
||||
dataIndex: "actual_cost",
|
||||
key: "actual_cost",
|
||||
sorter: (a, b) => a.actual_cost - b.actual_cost,
|
||||
width: "7rem",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_cost" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
@@ -64,6 +67,7 @@ export default function JobReconciliationBillsTable({
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
width: "4rem",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -72,9 +76,11 @@ export default function JobReconciliationBillsTable({
|
||||
dataIndex: "is_credit_memo",
|
||||
key: "is_credit_memo",
|
||||
sorter: (a, b) => a.bill.is_credit_memo - b.bill.is_credit_memo,
|
||||
width: "8rem",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||
state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => (
|
||||
<Checkbox disabled checked={record.bill.is_credit_memo} />
|
||||
),
|
||||
@@ -94,7 +100,7 @@ export default function JobReconciliationBillsTable({
|
||||
<Table
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ y: "80vh", x: true }}
|
||||
scroll={{ y: "60vh" }}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={invoiceLineData}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.imex-reconciliation-modal {
|
||||
top: 20px;
|
||||
.ant-modal-content {
|
||||
height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ant-modal-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
// .ant-modal-content {
|
||||
// height: 95vh;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// .ant-modal-body {
|
||||
// display: flex;
|
||||
// flex: 1;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function JobReconcilitionPartsTable({
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
ellipses: true,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -57,6 +58,7 @@ export default function JobReconcilitionPartsTable({
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
width: "7rem",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
|
||||
@@ -68,10 +70,12 @@ export default function JobReconcilitionPartsTable({
|
||||
title: t("joblines.fields.part_qty"),
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty",
|
||||
width: "4rem",
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
width: "7rem",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.act_price * a.part_qty - b.act_price * b.part_qty,
|
||||
sortOrder:
|
||||
@@ -89,6 +93,7 @@ export default function JobReconcilitionPartsTable({
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
width: "6rem",
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
},
|
||||
@@ -108,7 +113,7 @@ export default function JobReconcilitionPartsTable({
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
size="small"
|
||||
scroll={{ y: "80vh", x: true }}
|
||||
scroll={{ y: "60vh" }}
|
||||
rowKey="id"
|
||||
dataSource={jobLineData}
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Form, notification, Popover } from "antd";
|
||||
import { useMutation, useLazyQuery } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
InputNumber,
|
||||
notification,
|
||||
Popover,
|
||||
Space,
|
||||
} from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
|
||||
import {
|
||||
INSERT_SCOREBOARD_ENTRY,
|
||||
QUERY_SCOREBOARD_ENTRY,
|
||||
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";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
|
||||
export default function ScoreboardAddButton({
|
||||
job,
|
||||
@@ -15,17 +27,46 @@ export default function ScoreboardAddButton({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
|
||||
const [updateScoreboardEntry] = useMutation(UPDATE_SCOREBOARD_ENTRY);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const [callQuery, { loading: entryLoading, data: entryData }] = useLazyQuery(
|
||||
QUERY_SCOREBOARD_ENTRY
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visibility) {
|
||||
callQuery({ variables: { jobid: job.id } });
|
||||
}
|
||||
}, [visibility, job.id, callQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("UE", entryData);
|
||||
if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
|
||||
console.log("Setting FOrm");
|
||||
form.setFieldsValue(entryData.scoreboard[0]);
|
||||
}
|
||||
}, [entryData, form]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("job_close_add_to_scoreboard");
|
||||
|
||||
setLoading(true);
|
||||
const result = await insertScoreboardEntry({
|
||||
variables: { sbInput: [{ jobid: job.id, ...values }] },
|
||||
});
|
||||
let result;
|
||||
|
||||
if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
|
||||
result = await updateScoreboardEntry({
|
||||
variables: {
|
||||
sbId: entryData.scoreboard[0].id,
|
||||
sbInput: values,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
result = await insertScoreboardEntry({
|
||||
variables: { sbInput: [{ jobid: job.id, ...values }] },
|
||||
});
|
||||
}
|
||||
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
@@ -45,53 +86,62 @@ export default function ScoreboardAddButton({
|
||||
const overlay = (
|
||||
<Card>
|
||||
<div>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
initialValues={{}}
|
||||
>
|
||||
<Form.Item
|
||||
label={t("scoreboard.fields.date")}
|
||||
name="date"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
{entryLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
initialValues={{}}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("scoreboard.fields.bodyhrs")}
|
||||
name="bodyhrs"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumberCalculator precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("scoreboard.fields.painthrs")}
|
||||
name="painthrs"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumberCalculator precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("scoreboard.fields.date")}
|
||||
name="date"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("scoreboard.fields.bodyhrs")}
|
||||
name="bodyhrs"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("scoreboard.fields.painthrs")}
|
||||
name="painthrs"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Form>
|
||||
<Space wrap>
|
||||
<Button type="primary" htmlType="submit">
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
@@ -100,7 +150,7 @@ export default function ScoreboardAddButton({
|
||||
setLoading(true);
|
||||
const v = job.joblines.reduce(
|
||||
(acc, val) => {
|
||||
if (val.mod_lbr_ty === "LAB")
|
||||
if (val.mod_lbr_ty !== "LAR")
|
||||
acc = { ...acc, bodyhrs: acc.bodyhrs + val.mod_lb_hrs };
|
||||
if (val.mod_lbr_ty === "LAR")
|
||||
acc = { ...acc, painthrs: acc.painthrs + val.mod_lb_hrs };
|
||||
|
||||
@@ -22,10 +22,10 @@ export default function JobAdminDeleteIntake({ job }) {
|
||||
mutation DELETE_DELIVERY($jobId: uuid!) {
|
||||
update_jobs_by_pk(
|
||||
pk_columns: { id: $jobId }
|
||||
_set: { deliverychecklist: null }
|
||||
_set: { deliverchecklist: null }
|
||||
) {
|
||||
id
|
||||
deliverychecklist
|
||||
deliverchecklist
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { Col, notification, Row } from "antd";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||
@@ -90,13 +90,7 @@ export function JobsAvailableContainer({
|
||||
|
||||
const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk);
|
||||
|
||||
if (
|
||||
!(
|
||||
|
||||
estData &&
|
||||
estData.est_data
|
||||
)
|
||||
) {
|
||||
if (!(estData && estData.est_data)) {
|
||||
//We don't have the right data. Error!
|
||||
setInsertLoading(false);
|
||||
notification["error"]({
|
||||
@@ -104,6 +98,8 @@ export function JobsAvailableContainer({
|
||||
});
|
||||
return;
|
||||
}
|
||||
//IO-539 Check for Parts Rate on PAL for SGI use case.
|
||||
await CheckTaxRates(estData.est_data, bodyshop);
|
||||
|
||||
const newTotals = (
|
||||
await Axios.post("/job/totals", {
|
||||
@@ -115,10 +111,7 @@ export function JobsAvailableContainer({
|
||||
).data;
|
||||
|
||||
let existingVehicles;
|
||||
if (
|
||||
estData.est_data.vehicle &&
|
||||
estData.est_data.vin
|
||||
) {
|
||||
if (estData.est_data.vehicle && estData.est_data.vin) {
|
||||
//There's vehicle data, need to double check the VIN.
|
||||
existingVehicles = await client.query({
|
||||
query: SEARCH_VEHICLE_BY_VIN,
|
||||
@@ -147,16 +140,17 @@ export function JobsAvailableContainer({
|
||||
: {}),
|
||||
};
|
||||
|
||||
if (selectedOwner) {
|
||||
newJob.ownerid = selectedOwner;
|
||||
delete newJob.owner;
|
||||
}
|
||||
if (newJob.vehicleid) {
|
||||
delete newJob.vehicle;
|
||||
}
|
||||
|
||||
insertNewJob({
|
||||
variables: {
|
||||
job: selectedOwner
|
||||
? Object.assign(
|
||||
{},
|
||||
newJob,
|
||||
{ owner: null },
|
||||
{ ownerid: selectedOwner }
|
||||
)
|
||||
: newJob,
|
||||
job: newJob,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
@@ -196,14 +190,10 @@ export function JobsAvailableContainer({
|
||||
|
||||
setJobModalVisible(false);
|
||||
setInsertLoading(true);
|
||||
const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk);
|
||||
if (
|
||||
!(
|
||||
|
||||
estData &&
|
||||
estData.est_data
|
||||
)
|
||||
) {
|
||||
|
||||
const estData = estDataRaw.data.available_jobs_by_pk;
|
||||
|
||||
if (!(estData && estData.est_data)) {
|
||||
//We don't have the right data. Error!
|
||||
setInsertLoading(false);
|
||||
notification["error"]({
|
||||
@@ -211,18 +201,21 @@ export function JobsAvailableContainer({
|
||||
});
|
||||
} else {
|
||||
//create upsert job
|
||||
let supp = _.cloneDeep(estData.est_data);
|
||||
let supp = replaceEmpty({ ...estData.est_data });
|
||||
//IO-539 Check for Parts Rate on PAL for SGI use case.
|
||||
await CheckTaxRates(supp, bodyshop);
|
||||
|
||||
delete supp.owner;
|
||||
delete supp.vehicle;
|
||||
if (importOptions.overrideHeaders) {
|
||||
delete supp.ins_co_nm;
|
||||
if (!importOptions.overrideHeaders) {
|
||||
HeaderFields.forEach((item) => delete supp[item]);
|
||||
}
|
||||
|
||||
let suppDelta = await GetSupplementDelta(
|
||||
client,
|
||||
selectedJob,
|
||||
estData.est_data.joblines.data
|
||||
supp.joblines.data
|
||||
);
|
||||
|
||||
delete supp.joblines;
|
||||
@@ -394,10 +387,108 @@ export default connect(
|
||||
)(JobsAvailableContainer);
|
||||
|
||||
function replaceEmpty(someObj, replaceValue = null) {
|
||||
const replacer = (key, value) => (value === "" ? replaceValue : value);
|
||||
const replacer = (key, value) =>
|
||||
value === "" ? replaceValue || null : value;
|
||||
//^ because you seem to want to replace (strings) "null" or "undefined" too
|
||||
console.log(someObj)
|
||||
const temp = JSON.stringify(someObj, replacer);
|
||||
console.log(`temp`, temp);
|
||||
return JSON.parse(temp);
|
||||
}
|
||||
|
||||
async function CheckTaxRates(estData, bodyshop) {
|
||||
console.log(
|
||||
"🚀 ~ file: jobs-available-table.container.jsx ~ line 398 ~ estData",
|
||||
estData
|
||||
);
|
||||
//LKQ Check
|
||||
if (
|
||||
!estData.parts_tax_rates?.PAL ||
|
||||
estData.parts_tax_rates?.PAL?.prt_tax_rt === null ||
|
||||
estData.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.parts_tax_rates.PAL) {
|
||||
estData.parts_tax_rates.PAL = {
|
||||
prt_discp: 0,
|
||||
prt_mktyp: true,
|
||||
prt_mkupp: 0,
|
||||
prt_type: "PAL",
|
||||
};
|
||||
}
|
||||
estData.parts_tax_rates.PAL.prt_tax_rt =
|
||||
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||
estData.parts_tax_rates.PAL.prt_tax_in = true;
|
||||
}
|
||||
}
|
||||
//PAC Check
|
||||
if (
|
||||
!estData.parts_tax_rates?.PAC ||
|
||||
estData.parts_tax_rates?.PAC?.prt_tax_rt === null ||
|
||||
estData.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.parts_tax_rates.PAC) {
|
||||
estData.parts_tax_rates.PAC = {
|
||||
prt_discp: 0,
|
||||
prt_mktyp: true,
|
||||
prt_mkupp: 0,
|
||||
prt_type: "PAC",
|
||||
};
|
||||
}
|
||||
estData.parts_tax_rates.PAC.prt_tax_rt =
|
||||
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||
estData.parts_tax_rates.PAC.prt_tax_in = true;
|
||||
}
|
||||
}
|
||||
//PAM Check
|
||||
if (
|
||||
!estData.parts_tax_rates?.PAM ||
|
||||
estData.parts_tax_rates?.PAM?.prt_tax_rt === null ||
|
||||
estData.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.parts_tax_rates.PAM) {
|
||||
estData.parts_tax_rates.PAM = {
|
||||
prt_discp: 0,
|
||||
prt_mktyp: true,
|
||||
prt_mkupp: 0,
|
||||
prt_type: "PAM",
|
||||
};
|
||||
}
|
||||
estData.parts_tax_rates.PAM.prt_tax_rt =
|
||||
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||
estData.parts_tax_rates.PAM.prt_tax_in = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!estData.parts_tax_rates?.PAR ||
|
||||
estData.parts_tax_rates?.PAR?.prt_tax_rt === null ||
|
||||
estData.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.parts_tax_rates.PAR) {
|
||||
estData.parts_tax_rates.PAR = {
|
||||
prt_discp: 0,
|
||||
prt_mktyp: true,
|
||||
prt_mkupp: 0,
|
||||
prt_type: "PAR",
|
||||
};
|
||||
}
|
||||
estData.parts_tax_rates.PAR.prt_tax_rt =
|
||||
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||
estData.parts_tax_rates.PAR.prt_tax_in = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "antd";
|
||||
import { Button, Dropdown, Menu } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -12,11 +12,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const handleAllocate = () => {
|
||||
logImEXEvent("jobs_close_allocate_auto");
|
||||
|
||||
const { defaults } = bodyshop.md_responsibility_centers;
|
||||
|
||||
const handleAllocate = (defaults) => {
|
||||
form.setFieldsValue({
|
||||
joblines: joblines.map((jl) => {
|
||||
const ret = _.cloneDeep(jl);
|
||||
@@ -32,7 +29,7 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
}
|
||||
//Verify that this is also manually updated in server/job-costing
|
||||
if (!jl.part_type && !jl.mod_lbr_ty) {
|
||||
const lineDesc = jl.line_desc.toLowerCase();
|
||||
const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : "";
|
||||
if (lineDesc.includes("shop materials")) {
|
||||
ret.profitcenter_part = defaults.profits["MASH"];
|
||||
} else if (lineDesc.includes("paint/materials")) {
|
||||
@@ -48,8 +45,36 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleAllocate} disabled={disabled}>
|
||||
const handleAutoAllocateClick = () => {
|
||||
logImEXEvent("jobs_close_allocate_auto");
|
||||
|
||||
const { defaults } = bodyshop.md_responsibility_centers;
|
||||
handleAllocate(defaults);
|
||||
};
|
||||
|
||||
const handleMenuClick = ({ item, key, keyPath, domEvent }) => {
|
||||
logImEXEvent("jobs_close_allocate_auto_dms");
|
||||
handleAllocate(
|
||||
bodyshop.md_responsibility_centers.dms_defaults.find(
|
||||
(x) => x.name === key
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const overlay = bodyshop.cdk_dealerid && (
|
||||
<Menu onClick={handleMenuClick}>
|
||||
{bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => (
|
||||
<Menu.Item key={mapping.name}>{mapping.name}</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return bodyshop.cdk_dealerid ? (
|
||||
<Dropdown overlay={overlay}>
|
||||
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Button onClick={handleAutoAllocateClick} disabled={disabled}>
|
||||
{t("jobs.actions.autoallocate")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -24,57 +25,75 @@ export function JobsCloseExportButton({
|
||||
currentUser,
|
||||
jobId,
|
||||
disabled,
|
||||
setSelectedJobs,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleQbxml = async () => {
|
||||
//Check if it's a CDK setup.
|
||||
if (bodyshop.cdk_dealerid) {
|
||||
history.push(`/manage/dms?jobId=${jobId}`);
|
||||
return;
|
||||
}
|
||||
logImEXEvent("jobs_close_export");
|
||||
|
||||
setLoading(true);
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/receivables",
|
||||
{ jobIds: [jobId] },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log("handle -> XML", QbXmlResponse);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if it's a QBO Setup.
|
||||
let PartnerResponse;
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
// "http://609feaeae986.ngrok.io/qb/",
|
||||
QbXmlResponse.data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting-partner"),
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
||||
withCredentials: true,
|
||||
jobIds: [jobId],
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
} else {
|
||||
//Default is QBD
|
||||
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/receivables",
|
||||
{ jobIds: [jobId] },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log("handle -> XML", QbXmlResponse);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
// "http://609feaeae986.ngrok.io/qb/",
|
||||
QbXmlResponse.data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting-partner"),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("PartnerResponse", PartnerResponse);
|
||||
@@ -147,18 +166,18 @@ export function JobsCloseExportButton({
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (setSelectedJobs) {
|
||||
setSelectedJobs((selectedJobs) => {
|
||||
return selectedJobs.filter((i) => i !== jobId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleQbxml}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
type="dashed"
|
||||
>
|
||||
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||
{t("jobs.actions.export")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@ export function JobsDetailHeaderCsi({
|
||||
replyTo: bodyshop.email,
|
||||
},
|
||||
template: {
|
||||
name: TemplateList("job").csi_invitation.key,
|
||||
name: TemplateList("job_special").csi_invitation_action.key,
|
||||
variables: {
|
||||
id: result.data.insert_csi.returning[0].id,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
@@ -75,11 +76,15 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
{job.ins_co_nm}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||
{job.po_number}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<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 +182,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>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CABCpvrtCalculator from "../ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||
@@ -22,9 +23,10 @@ import JobsDetailRatesParts from "./jobs-detail-rates.parts.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function JobsDetailRates({ jobRO, form, job }) {
|
||||
export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
@@ -77,7 +79,7 @@ export function JobsDetailRates({ jobRO, form, job }) {
|
||||
label={t("jobs.fields.adjustment_bottom_line")}
|
||||
name="adjustment_bottom_line"
|
||||
>
|
||||
<CurrencyInput disabled={jobRO} />
|
||||
<CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} />
|
||||
</Form.Item>
|
||||
<Space align="end">
|
||||
<Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt">
|
||||
|
||||
@@ -34,53 +34,61 @@ export function JobsExportAllButton({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleQbxml = async () => {
|
||||
logImEXEvent("jobs_export_all");
|
||||
|
||||
setLoading(true);
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/receivables",
|
||||
{ jobIds: jobIds },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let PartnerResponse;
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
// "http://609feaeae986.ngrok.io/qb/",
|
||||
QbXmlResponse.data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting-partner"),
|
||||
setLoading(true);
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
||||
withCredentials: true,
|
||||
jobIds: jobIds,
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/receivables",
|
||||
{ jobIds: jobIds },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
// "http://609feaeae986.ngrok.io/qb/",
|
||||
QbXmlResponse.data,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.exporting-partner"),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log("PartnerResponse", PartnerResponse);
|
||||
const groupedData = _.groupBy(PartnerResponse.data, "id");
|
||||
const groupedData = _.groupBy(
|
||||
PartnerResponse.data,
|
||||
bodyshop.accountingconfig.qbo ? "jobid" : "id"
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(groupedData).map(async (key) => {
|
||||
@@ -157,6 +165,7 @@ export function JobsExportAllButton({
|
||||
|
||||
if (!!completedCallback) completedCallback([]);
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -130,13 +130,10 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
ellipsis: true,
|
||||
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||
render: (text, record) => {
|
||||
return record.clm_no ? (
|
||||
<span>{record.clm_no}</span>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
},
|
||||
render: (text, record) =>
|
||||
`${record.clm_no || ""}${
|
||||
record.po_number ? ` (PO: ${record.po_number})` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
|
||||
@@ -208,6 +208,10 @@ export function JobsList({ bodyshop }) {
|
||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
`${record.clm_no || ""}${
|
||||
record.po_number ? ` (PO: ${record.po_number})` : ""
|
||||
}`,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
|
||||
@@ -10,8 +10,21 @@ import {
|
||||
QUERY_NOTES_BY_JOB_PK,
|
||||
} from "../../graphql/notes.queries";
|
||||
import JobNotesComponent from "./jobs.notes.component";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
export default function JobNotesContainer({ jobId }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
|
||||
|
||||
export function JobNotesContainer({ jobId, insertAuditTrail }) {
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_NOTES_BY_JOB_PK, {
|
||||
variables: { id: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
@@ -32,6 +45,10 @@ export default function JobNotesContainer({ jobId }) {
|
||||
notification["success"]({
|
||||
message: t("notes.successes.deleted"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobnotedeleted(),
|
||||
});
|
||||
});
|
||||
setDeleteLoading(false);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function JobsRelatedRos({ jobid, job }) {
|
||||
if (!(job && job.vehicle && job.vehicle.jobs)) return null;
|
||||
return (
|
||||
<Space wrap>
|
||||
{job.vehicle.jobs
|
||||
.filter((j) => j.id !== job.id)
|
||||
.map((j) => (
|
||||
<Tag key={j.id}>
|
||||
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
j.clm_no ? ` | ${j.clm_no}` : ""
|
||||
}${j.status ? ` | ${j.status}` : ""}`}</Link>
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { alphaSort } from "../../utils/sorters";
|
||||
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
|
||||
import "./labor-allocations-table.styles.scss";
|
||||
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
|
||||
|
||||
import _ from "lodash";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician,
|
||||
@@ -57,6 +57,7 @@ export function LaborAllocationsTable({
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||
render: (text, record) => `${record.cost_center} (${record.mod_lbr_ty})`,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.hrs_total"),
|
||||
@@ -113,7 +114,7 @@ export function LaborAllocationsTable({
|
||||
color: record.difference >= 0 ? "green" : "red",
|
||||
}}
|
||||
>
|
||||
{record.difference}
|
||||
{_.round(record.difference, 1)}
|
||||
</strong>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ export const CalculateAllocationsTotals = (
|
||||
const r = {
|
||||
opcode: value,
|
||||
cost_center: responsibilitycenters.defaults.costs[value],
|
||||
mod_lbr_ty: value,
|
||||
total: joblines.reduce((acc2, val2) => {
|
||||
return val2.mod_lbr_ty === value ? acc2 + val2.mod_lb_hrs : acc2;
|
||||
}, 0),
|
||||
|
||||
@@ -10,6 +10,8 @@ import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -17,12 +19,15 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export function NoteUpsertModalContainer({
|
||||
currentUser,
|
||||
noteUpsertModal,
|
||||
toggleModalVisible,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [insertNote] = useMutation(INSERT_NEW_NOTE);
|
||||
@@ -56,6 +61,10 @@ export function NoteUpsertModalContainer({
|
||||
notification["success"]({
|
||||
message: t("notes.successes.updated"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteupdated(),
|
||||
});
|
||||
});
|
||||
if (refetch) refetch();
|
||||
toggleModalVisible();
|
||||
@@ -75,6 +84,10 @@ export function NoteUpsertModalContainer({
|
||||
notification["success"]({
|
||||
message: t("notes.successes.create"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: context.jobId,
|
||||
operation: AuditTrailMapping.jobnoteadded(),
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,6 +106,12 @@ export default function PartsOrderModalComponent({
|
||||
label={t("parts_orders.fields.quantity")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
|
||||
@@ -176,7 +176,7 @@ export function PartsOrderModalContainer({
|
||||
|
||||
if (refetch) refetch();
|
||||
toggleModalVisible();
|
||||
const Templates = TemplateList("partsorder");
|
||||
const Templates = TemplateList("partsorder", context);
|
||||
|
||||
if (sendType === "e") {
|
||||
const matchingVendor = data.vendors.filter(
|
||||
|
||||
@@ -35,52 +35,61 @@ export function PayableExportAll({
|
||||
|
||||
const handleQbxml = async () => {
|
||||
logImEXEvent("accounting_payables_export_all");
|
||||
let PartnerResponse;
|
||||
|
||||
setLoading(true);
|
||||
if (!!loadingCallback) loadingCallback(true);
|
||||
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/payables",
|
||||
{ bills: billids },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
||||
withCredentials: true,
|
||||
bills: billids,
|
||||
});
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/payables",
|
||||
{ bills: billids },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let PartnerResponse;
|
||||
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
QbXmlResponse.data
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting-partner"),
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
QbXmlResponse.data
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting-partner"),
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("handleQbxml -> PartnerResponse", PartnerResponse);
|
||||
const groupedData = _.groupBy(PartnerResponse.data, "id");
|
||||
const groupedData = _.groupBy(
|
||||
PartnerResponse.data,
|
||||
bodyshop.accountingconfig.qbo ? "billid" : "id"
|
||||
);
|
||||
|
||||
const proms = [];
|
||||
Object.keys(groupedData).forEach((key) => {
|
||||
proms.push(
|
||||
|
||||
@@ -25,6 +25,7 @@ export function PayableExportButton({
|
||||
billId,
|
||||
disabled,
|
||||
loadingCallback,
|
||||
setSelectedBills,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updateBill] = useMutation(UPDATE_BILLS);
|
||||
@@ -37,44 +38,53 @@ export function PayableExportButton({
|
||||
setLoading(true);
|
||||
if (!!loadingCallback) loadingCallback(true);
|
||||
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/payables",
|
||||
{ bills: [billId] },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if it's a QBO Setup.
|
||||
let PartnerResponse;
|
||||
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
QbXmlResponse.data
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting-partner"),
|
||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||
PartnerResponse = await axios.post(`/qbo/payables`, {
|
||||
withCredentials: true,
|
||||
bills: [billId],
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
} else {
|
||||
//Default is QBD
|
||||
|
||||
let QbXmlResponse;
|
||||
try {
|
||||
QbXmlResponse = await axios.post(
|
||||
"/accounting/qbxml/payables",
|
||||
{ bills: [billId] },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error getting QBXML from Server.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting", {
|
||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||
}),
|
||||
});
|
||||
if (loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
PartnerResponse = await axios.post(
|
||||
"http://localhost:1337/qb/",
|
||||
QbXmlResponse.data
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Error connecting to quickbooks or partner.", error);
|
||||
notification["error"]({
|
||||
message: t("bills.errors.exporting-partner"),
|
||||
});
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("handleQbxml -> PartnerResponse", PartnerResponse);
|
||||
@@ -122,7 +132,14 @@ export function PayableExportButton({
|
||||
});
|
||||
const billUpdateResponse = await updateBill({
|
||||
variables: {
|
||||
billIdList: successfulTransactions.map((st) => st.id),
|
||||
billIdList: successfulTransactions.map(
|
||||
(st) =>
|
||||
st[
|
||||
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
||||
? "billid"
|
||||
: "id"
|
||||
]
|
||||
),
|
||||
bill: {
|
||||
exported: true,
|
||||
exported_at: new Date(),
|
||||
@@ -142,6 +159,11 @@ export function PayableExportButton({
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (setSelectedBills) {
|
||||
setSelectedBills((selectedBills) => {
|
||||
return selectedBills.filter((i) => i !== billId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
|
||||
@@ -24,6 +24,7 @@ export function PaymentExportButton({
|
||||
paymentId,
|
||||
disabled,
|
||||
loadingCallback,
|
||||
setSelectedPayments,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [updatePayment] = useMutation(UPDATE_PAYMENTS);
|
||||
@@ -141,6 +142,12 @@ export function PaymentExportButton({
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (setSelectedPayments) {
|
||||
setSelectedPayments((selectedBills) => {
|
||||
return selectedBills.filter((i) => i !== paymentId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!!loadingCallback) loadingCallback(false);
|
||||
|
||||
@@ -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"]({
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
import { Card, Col, Input, Row, Space, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectPrintCenter } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
|
||||
import PrintCenterItem from "../print-center-item/print-center-item.component";
|
||||
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
printCenterModal: selectPrintCenter,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function PrintCenterJobsComponent({ printCenterModal }) {
|
||||
export function PrintCenterJobsComponent({ printCenterModal, bodyshop }) {
|
||||
const [search, setSearch] = useState("");
|
||||
const { id: jobId } = printCenterModal.context;
|
||||
const tempList = TemplateList("job", {});
|
||||
const { t } = useTranslation();
|
||||
const JobsReportsList = Object.keys(tempList).map((key) => {
|
||||
return tempList[key];
|
||||
});
|
||||
const JobsReportsList = Object.keys(tempList)
|
||||
.map((key) => {
|
||||
return tempList[key];
|
||||
})
|
||||
.filter(
|
||||
(temp) =>
|
||||
!temp.regions || (temp.regions && temp.regions[bodyshop.region_config])
|
||||
);
|
||||
|
||||
const filteredJobsReportsList =
|
||||
search !== ""
|
||||
|
||||
@@ -155,7 +155,7 @@ export function ProductionListTable({
|
||||
// }
|
||||
// };
|
||||
|
||||
if (!!!columns || columns.length === 0) return <div>No columns found.</div>;
|
||||
if (!!!columns) return <div>No columns found.</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Space, Tag } from "antd";
|
||||
import Axios from "axios";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect } from "react";
|
||||
import { useCookies } from "react-cookie";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import QboSignIn from "../../assets/qbo/C2QB_green_btn_med_default.svg";
|
||||
|
||||
export default function QboAuthorizeComponent() {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const [cookies, setCookie] = useCookies(["access_token", "refresh_token"]);
|
||||
|
||||
const handleQbSignIn = async () => {
|
||||
const result = await Axios.post("/qbo/authorize");
|
||||
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,
|
||||
});
|
||||
|
||||
history.push({ pathname: `/manage/accounting/receivables` });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [qs, location, setCookie]);
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<img
|
||||
onClick={handleQbSignIn}
|
||||
alt="Sign In to QuickBooks Online"
|
||||
src={QboSignIn}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
{!cookies.qbo_realmId && (
|
||||
<Tag color="red">No QuickBooks company has been connected.</Tag>
|
||||
)}
|
||||
{error && JSON.parse(decodeURIComponent(error)).error_description}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
BIN
client/src/components/qbo-authorize/qbo_signin.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -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,
|
||||
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Button, DatePicker, Form, Radio, Space } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
DatePicker,
|
||||
Form,
|
||||
Input,
|
||||
Radio,
|
||||
Row,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,7 +25,6 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import "./report-center-modal.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportCenterModal: selectReportCenter,
|
||||
});
|
||||
@@ -28,9 +38,14 @@ export default connect(
|
||||
|
||||
export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
const [form] = Form.useForm();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const Templates = TemplateList("report_center");
|
||||
const ReportsList = Object.keys(Templates).map((key) => {
|
||||
return Templates[key];
|
||||
});
|
||||
const { visible } = reportCenterModal;
|
||||
|
||||
const [callVendorQuery, { data: vendorData, called: vendorCalled }] =
|
||||
@@ -67,6 +82,9 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
...(end
|
||||
? { end: moment(end).endOf("day").format("YYYY-MM-DD") }
|
||||
: {}),
|
||||
...(start ? { starttz: moment(start).startOf("day") } : {}),
|
||||
...(end ? { endtz: moment(end).endOf("day") } : {}),
|
||||
|
||||
...(id ? { id: id } : {}),
|
||||
},
|
||||
},
|
||||
@@ -80,6 +98,17 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const FilteredReportsList =
|
||||
search !== ""
|
||||
? ReportsList.filter((r) =>
|
||||
r.title.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
: ReportsList;
|
||||
|
||||
//Group it, create cards, and then filter out.
|
||||
|
||||
const grouped = _.groupBy(FilteredReportsList, "group");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form
|
||||
@@ -88,6 +117,10 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
layout="vertical"
|
||||
form={form}
|
||||
>
|
||||
<Input.Search
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
value={search}
|
||||
/>
|
||||
<Form.Item
|
||||
name="key"
|
||||
label={t("reportcenter.labels.key")}
|
||||
@@ -100,23 +133,42 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Space
|
||||
direction="vertical"
|
||||
wrap
|
||||
size="small"
|
||||
style={{
|
||||
maxHeight: "50vh",
|
||||
}}
|
||||
>
|
||||
{Object.keys(Templates).map((key) => (
|
||||
{/* {Object.keys(Templates).map((key) => (
|
||||
<Radio key={key} value={key}>
|
||||
{Templates[key].title}
|
||||
</Radio>
|
||||
))} */}
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{Object.keys(grouped).map((key) => (
|
||||
<Col md={8} sm={12} key={key}>
|
||||
<Card.Grid
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
maxHeight: "33vh",
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
>
|
||||
<Typography.Title level={4}>
|
||||
{t(`reportcenter.labels.groups.${key}`)}
|
||||
</Typography.Title>
|
||||
<ul style={{ columns: "2 auto" }}>
|
||||
{grouped[key].map((item) => (
|
||||
<li key={item.key}>
|
||||
<Radio key={item.key} value={item.key}>
|
||||
{item.title}
|
||||
</Radio>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card.Grid>
|
||||
</Col>
|
||||
))}
|
||||
</Space>
|
||||
</Row>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={["key"]}>
|
||||
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
if (!key) return null;
|
||||
@@ -137,14 +189,14 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
shouldUpdate={(prev, cur) =>
|
||||
Templates[prev.key]?.idtype !== Templates[cur.key]?.idtype
|
||||
}
|
||||
style={{ display: "none" }}
|
||||
style={{ display: "none", margin: 0, padding: 0 }}
|
||||
>
|
||||
{() => {
|
||||
form.setFieldsValue({ id: null });
|
||||
return null;
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item dependencies={["key"]}>
|
||||
<Form.Item style={{ margin: 0, padding: 0 }} dependencies={["key"]}>
|
||||
{() => {
|
||||
const key = form.getFieldValue("key");
|
||||
if (!key) return null;
|
||||
@@ -204,7 +256,7 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
|
||||
]}
|
||||
>
|
||||
<DatePicker.RangePicker
|
||||
format="YYYY-MM-DD"
|
||||
format="MM/DD/YYYY"
|
||||
ranges={DatePIckerRanges}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||
import _ from "lodash";
|
||||
|
||||
const graphProps = {
|
||||
strokeWidth: 3,
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -51,18 +56,23 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
}
|
||||
|
||||
const theValue = {
|
||||
date: moment(val).format("D dd"),
|
||||
paintHrs: dayhrs.painthrs,
|
||||
bodyHrs: dayhrs.bodyhrs,
|
||||
accTargetHrs: Utils.AsOfDateTargetHours(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget +
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
val
|
||||
date: moment(val).format("D ddd"),
|
||||
paintHrs: _.round(dayhrs.painthrs, 1),
|
||||
bodyHrs: _.round(dayhrs.bodyhrs, 1),
|
||||
accTargetHrs: _.round(
|
||||
Utils.AsOfDateTargetHours(
|
||||
bodyshop.scoreboard_target.dailyBodyTarget +
|
||||
bodyshop.scoreboard_target.dailyPaintTarget,
|
||||
val
|
||||
),
|
||||
1
|
||||
),
|
||||
accHrs:
|
||||
accHrs: _.round(
|
||||
acc.length > 0
|
||||
? acc[acc.length - 1].accHrs + dayhrs.painthrs + dayhrs.bodyhrs
|
||||
: dayhrs.painthrs + dayhrs.bodyhrs,
|
||||
1
|
||||
),
|
||||
};
|
||||
|
||||
return [...acc, theValue];
|
||||
@@ -76,36 +86,37 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
>
|
||||
<CartesianGrid stroke="#f5f5f5" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
|
||||
<YAxis strokeWidth={graphProps.strokeWidth} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Area
|
||||
type="monotone"
|
||||
name="Accumulated Hours"
|
||||
dataKey="accHrs"
|
||||
fill="#8884d8"
|
||||
stroke="#8884d8"
|
||||
fill="lightgreen"
|
||||
stroke="green"
|
||||
/>
|
||||
<Bar
|
||||
name="Body Hours"
|
||||
dataKey="bodyHrs"
|
||||
stackId="day"
|
||||
barSize={20}
|
||||
fill="#cecece"
|
||||
fill="darkblue"
|
||||
/>
|
||||
<Bar
|
||||
name="Paint Hours"
|
||||
dataKey="paintHrs"
|
||||
stackId="day"
|
||||
barSize={20}
|
||||
fill="#413ea0"
|
||||
fill="darkred"
|
||||
/>
|
||||
<Line
|
||||
name="Target Hours"
|
||||
type="monotone"
|
||||
dataKey="accTargetHrs"
|
||||
stroke="#ff7300"
|
||||
strokeWidth={graphProps.strokeWidth}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
import { Col, Row } from "antd";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
|
||||
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
|
||||
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
|
||||
|
||||
export default function ScoreboardDisplayComponent({ scoreboardSubscription }) {
|
||||
const { data } = scoreboardSubscription;
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { GET_BLOCKED_DAYS } from "../../graphql/scoreboard.queries";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScoreboardDisplayComponent);
|
||||
|
||||
export function ScoreboardDisplayComponent({
|
||||
bodyshop,
|
||||
scoreboardSubscription,
|
||||
}) {
|
||||
const { data } = scoreboardSubscription;
|
||||
const client = useApolloClient();
|
||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
||||
|
||||
const sbEntriesByDate = {};
|
||||
@@ -19,6 +40,29 @@ export default function ScoreboardDisplayComponent({ scoreboardSubscription }) {
|
||||
sbEntriesByDate[entryDate].push(i);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
//Update the locals.
|
||||
async function setMomentSettings() {
|
||||
const {
|
||||
data: { appointments },
|
||||
} = await client.query({
|
||||
query: GET_BLOCKED_DAYS,
|
||||
variables: {
|
||||
start: moment().startOf("month"),
|
||||
end: moment().endOf("month"),
|
||||
},
|
||||
});
|
||||
|
||||
moment.updateLocale("ca", {
|
||||
workingWeekdays: translateSettingsToWorkingDays(bodyshop.workingdays),
|
||||
holidays: appointments.map((h) => moment(h.start).format("MM-DD-YYYY")),
|
||||
holidayFormat: "MM-DD-YYYY",
|
||||
});
|
||||
}
|
||||
|
||||
setMomentSettings();
|
||||
}, [client, bodyshop]);
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
@@ -35,3 +79,30 @@ export default function ScoreboardDisplayComponent({ scoreboardSubscription }) {
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function translateSettingsToWorkingDays(workingdays) {
|
||||
const days = [];
|
||||
|
||||
if (workingdays.monday) {
|
||||
days.push(1);
|
||||
}
|
||||
if (workingdays.tuesday) {
|
||||
days.push(2);
|
||||
}
|
||||
if (workingdays.wednesday) {
|
||||
days.push(3);
|
||||
}
|
||||
if (workingdays.thursday) {
|
||||
days.push(4);
|
||||
}
|
||||
if (workingdays.friday) {
|
||||
days.push(5);
|
||||
}
|
||||
if (workingdays.saturday) {
|
||||
days.push(6);
|
||||
}
|
||||
if (workingdays.sunday) {
|
||||
days.push(0);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import moment from "moment-business-days";
|
||||
|
||||
moment.updateLocale("ca", {
|
||||
workingWeekdays: [1, 2, 3, 4, 5],
|
||||
});
|
||||
// moment.updateLocale("ca", {
|
||||
// workingWeekdays: [1, 2, 3, 4, 5, 6],
|
||||
// });
|
||||
|
||||
export const CalculateWorkingDaysThisMonth = () => {
|
||||
return moment().endOf("month").businessDaysIntoMonth();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Button, Card, Tabs } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ShopInfoGeneral from "./shop-info.general.component";
|
||||
import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component";
|
||||
import ShopInfoLaborRates from "./shop-info.laborrates.component";
|
||||
@@ -11,7 +14,15 @@ import ShopInfoROStatusComponent from "./shop-info.rostatus.component";
|
||||
import ShopInfoSchedulingComponent from "./shop-info.scheduling.component";
|
||||
import ShopInfoSpeedPrint from "./shop-info.speedprint.component";
|
||||
|
||||
export default function ShopInfoComponent({ form, saveLoading }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent);
|
||||
|
||||
export function ShopInfoComponent({ bodyshop, form, saveLoading }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Card
|
||||
@@ -53,6 +64,7 @@ export default function ShopInfoComponent({ form, saveLoading }) {
|
||||
>
|
||||
<ShopInfoResponsibilityCenterComponent form={form} />
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane key="checklists" tab={t("bodyshop.labels.checklists")}>
|
||||
<ShopInfoIntakeChecklistComponent form={form} />
|
||||
</Tabs.TabPane>
|
||||
|
||||
@@ -121,6 +121,13 @@ export default function ShopInfoGeneral({ form }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")}>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
@@ -165,6 +172,20 @@ export default function ShopInfoGeneral({ form }) {
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Card, Space, Table } from "antd";
|
||||
import { EditFilled } from "@ant-design/icons";
|
||||
import { Card, Space, Table } from "antd";
|
||||
import moment from "moment";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
selectAuthLevel,
|
||||
@@ -16,8 +17,6 @@ import RbacWrapper, {
|
||||
HasRbacAccess,
|
||||
} from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
authLevel: selectAuthLevel,
|
||||
@@ -268,8 +267,12 @@ export function TimeTicketList({
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell>{totals.productivehrs}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{totals.actualhrs}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{totals.productivehrs.toFixed(1)}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{totals.actualhrs.toFixed(1)}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>
|
||||
{totals.actualhrs === 0 || !totals.actualhrs
|
||||
? "∞"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Form, Input, InputNumber, Select } from "antd";
|
||||
import { Form, Input, InputNumber, Select, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -48,7 +48,9 @@ export function TimeTicketModalComponent({
|
||||
{emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center}>
|
||||
{item.cost_center}
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
@@ -111,7 +113,17 @@ export function TimeTicketModalComponent({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<EmployeeSearchSelect options={employeeAutoCompleteOptions} />
|
||||
<EmployeeSearchSelect
|
||||
options={employeeAutoCompleteOptions}
|
||||
onSelect={(value) => {
|
||||
console.log(value);
|
||||
const emps =
|
||||
employeeAutoCompleteOptions &&
|
||||
employeeAutoCompleteOptions.filter((e) => e.id === value)[0];
|
||||
console.log(emps);
|
||||
form.setFieldsValue({ flat_rate: emps && emps.flat_rate });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(prev, cur) => prev.employeeid !== cur.employeeid}
|
||||
@@ -138,6 +150,14 @@ export function TimeTicketModalComponent({
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="flat_rate"
|
||||
label={t("timetickets.fields.flat_rate")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LayoutFormRow>
|
||||
|
||||
@@ -132,8 +132,8 @@ const JobRelatedTicketsTable = ({
|
||||
return {
|
||||
id: `${item.jobKey}${costCenter}`,
|
||||
item,
|
||||
actHrs,
|
||||
prodHrs,
|
||||
actHrs: actHrs.toFixed(1),
|
||||
prodHrs: prodHrs.toFixed(1),
|
||||
clockHrs,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function VendorsFormComponent({
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber />
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
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, updatePassword, updateProfile } from "firebase/auth";
|
||||
import { getFirestore } from "firebase/firestore";
|
||||
import { tracker } from "../App/App.container";
|
||||
//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) => {
|
||||
@@ -27,7 +26,7 @@ export const getCurrentUser = () => {
|
||||
export const updateCurrentUser = (userDetails) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const unsubscribe = auth.onAuthStateChanged((userAuth) => {
|
||||
userAuth.updateProfile(userDetails).then((r) => {
|
||||
updateProfile(userAuth, userDetails).then((r) => {
|
||||
unsubscribe();
|
||||
resolve(userAuth);
|
||||
});
|
||||
@@ -38,32 +37,20 @@ export const updateCurrentUser = (userDetails) => {
|
||||
export const updateCurrentPassword = async (password) => {
|
||||
const currentUser = await getCurrentUser();
|
||||
|
||||
return currentUser.updatePassword(password);
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// const unsubscribe = auth.onAuthStateChanged(
|
||||
// (userAuth) => {
|
||||
// userAuth.updatePassword(password).then((r) => {
|
||||
// unsubscribe();
|
||||
// resolve(userAuth);
|
||||
// });
|
||||
// },
|
||||
// (error) => reject(error)
|
||||
// );
|
||||
// });
|
||||
return updatePassword(currentUser, 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();
|
||||
@@ -76,65 +63,68 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
null,
|
||||
...additionalParams,
|
||||
};
|
||||
console.log(
|
||||
"%c[Analytics]",
|
||||
"background-color: green ;font-weight:bold;",
|
||||
eventName,
|
||||
eventParams
|
||||
);
|
||||
analytics.logEvent(eventName, eventParams);
|
||||
// console.log(
|
||||
// "%c[Analytics]",
|
||||
// "background-color: green ;font-weight:bold;",
|
||||
// eventName,
|
||||
// eventParams
|
||||
// );
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
|
||||
//Log event to OpenReplay server.
|
||||
tracker.event(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);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
@@ -24,6 +24,10 @@ export const QUERY_JOBS_FOR_EXPORT = gql`
|
||||
clm_total
|
||||
clm_no
|
||||
ins_co_nm
|
||||
exportlogs {
|
||||
id
|
||||
successful
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -37,6 +41,10 @@ export const QUERY_BILLS_FOR_EXPORT = gql`
|
||||
invoice_number
|
||||
is_credit_memo
|
||||
total
|
||||
exportlogs {
|
||||
id
|
||||
successful
|
||||
}
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
@@ -73,6 +81,10 @@ export const QUERY_PAYMENTS_FOR_EXPORT = gql`
|
||||
transactionid
|
||||
paymentnum
|
||||
date
|
||||
exportlogs {
|
||||
id
|
||||
successful
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -93,6 +93,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
features
|
||||
attach_pdf_to_email
|
||||
tt_allow_post_to_invoiced
|
||||
cdk_configuration
|
||||
employees {
|
||||
id
|
||||
active
|
||||
@@ -182,6 +183,7 @@ export const UPDATE_SHOP = gql`
|
||||
cdk_dealerid
|
||||
attach_pdf_to_email
|
||||
tt_allow_post_to_invoiced
|
||||
cdk_configuration
|
||||
employees {
|
||||
id
|
||||
first_name
|
||||
|
||||
@@ -9,6 +9,7 @@ export const CONVERSATION_LIST_SUBSCRIPTION = gql`
|
||||
) {
|
||||
phone_num
|
||||
id
|
||||
updated_at
|
||||
job_conversations {
|
||||
job {
|
||||
id
|
||||
|
||||
13
client/src/graphql/dms.queries.js
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -58,6 +58,7 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
||||
jobid
|
||||
employeeid
|
||||
memo
|
||||
flat_rate
|
||||
employee {
|
||||
id
|
||||
first_name
|
||||
@@ -159,7 +160,8 @@ export const UPDATE_JOB_LINE = gql`
|
||||
|
||||
export const GET_JOB_LINES_TO_ENTER_BILL = gql`
|
||||
query GET_JOB_LINES_TO_ENTER_BILL($id: uuid!) {
|
||||
joblines(where: { jobid: { _eq: $id }, removed: { _eq: false } }) {
|
||||
joblines(where: { jobid: { _eq: $id } }) {
|
||||
removed
|
||||
id
|
||||
line_desc
|
||||
part_type
|
||||
|
||||
@@ -31,6 +31,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
|
||||
id
|
||||
ins_co_nm
|
||||
clm_no
|
||||
po_number
|
||||
clm_total
|
||||
owner_owing
|
||||
ro_number
|
||||
@@ -76,6 +77,7 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
) {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ea
|
||||
plate_no
|
||||
@@ -97,6 +99,7 @@ export const QUERY_PARTS_QUEUE = gql`
|
||||
status
|
||||
updated_at
|
||||
vehicleid
|
||||
ownerid
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -109,6 +112,7 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
clm_no
|
||||
@@ -318,6 +322,7 @@ export const QUERY_JOB_COSTING_DETAILS = gql`
|
||||
cost_center
|
||||
actualhrs
|
||||
productivehrs
|
||||
flat_rate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,6 +366,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
converted
|
||||
lbr_adjustments
|
||||
ro_number
|
||||
po_number
|
||||
clm_total
|
||||
inproduction
|
||||
vehicleid
|
||||
@@ -383,6 +389,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
|
||||
@@ -433,6 +445,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
job_totals
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
@@ -448,6 +461,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
id
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
@@ -529,6 +543,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
db_ref
|
||||
manual_line
|
||||
prt_dsmk_p
|
||||
prt_dsmk_m
|
||||
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
||||
id
|
||||
quantity
|
||||
@@ -667,6 +682,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
inproduction
|
||||
production_vars
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ea
|
||||
ca_gst_registrant
|
||||
@@ -699,11 +715,16 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
v_model_desc
|
||||
v_color
|
||||
plate_no
|
||||
jobs {
|
||||
id
|
||||
clm_no
|
||||
ro_number
|
||||
}
|
||||
}
|
||||
actual_completion
|
||||
actual_delivery
|
||||
actual_in
|
||||
|
||||
po_number
|
||||
id
|
||||
ins_co_nm
|
||||
ins_ct_fn
|
||||
@@ -1619,6 +1640,7 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
|
||||
) {
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownerid
|
||||
ownr_ph1
|
||||
ownr_ea
|
||||
@@ -1648,6 +1670,7 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
|
||||
clm_total
|
||||
owner_owing
|
||||
ro_number
|
||||
po_number
|
||||
scheduled_completion
|
||||
scheduled_in
|
||||
scheduled_delivery
|
||||
@@ -1725,6 +1748,8 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
actual_delivery
|
||||
scheduled_in
|
||||
actual_in
|
||||
kmin
|
||||
kmout
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
removed
|
||||
@@ -1849,6 +1874,10 @@ export const QUERY_JOB_CHECKLISTS = gql`
|
||||
scheduled_delivery
|
||||
actual_delivery
|
||||
production_vars
|
||||
owner {
|
||||
id
|
||||
allow_text_message
|
||||
}
|
||||
bodyshop {
|
||||
id
|
||||
intakechecklist
|
||||
@@ -1871,3 +1900,69 @@ export const FIND_JOBS_BY_CLAIM = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_JOB_EXPORT_DMS = gql`
|
||||
query QUERY_JOB_EXPORT_DMS($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
id
|
||||
ro_number
|
||||
po_number
|
||||
clm_no
|
||||
job_totals
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
kmin
|
||||
kmout
|
||||
v_make_desc
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
area_of_damage
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const QUERY_RELATED_ROS = gql`
|
||||
query QUERY_RELATED_ROS($jobid: uuid!) {
|
||||
relatedjobs(
|
||||
where: {
|
||||
_or: [{ childjob: { _eq: $jobid } }, { parentjob: { _eq: $jobid } }]
|
||||
}
|
||||
) {
|
||||
parentjob
|
||||
id
|
||||
parentjob_rel {
|
||||
id
|
||||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,10 @@ import { gql } from "@apollo/client";
|
||||
|
||||
export const SUBSCRIPTION_SCOREBOARD = gql`
|
||||
subscription SUBSCRIPTION_SCOREBOARD($start: date!, $end: date!) {
|
||||
scoreboard(where: { _and: { date: { _gte: $start, _lte: $end } } }) {
|
||||
scoreboard(
|
||||
where: { _and: { date: { _gte: $start, _lte: $end } } }
|
||||
order_by: { date: asc }
|
||||
) {
|
||||
id
|
||||
painthrs
|
||||
bodyhrs
|
||||
@@ -48,3 +51,34 @@ export const UPDATE_SCOREBOARD_ENTRY = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_SCOREBOARD_ENTRY = gql`
|
||||
query QUERY_SCOREBOARD_ENTRY($jobid: uuid!) {
|
||||
scoreboard(where: { jobid: { _eq: $jobid } }) {
|
||||
bodyhrs
|
||||
date
|
||||
id
|
||||
painthrs
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_BLOCKED_DAYS = gql`
|
||||
query GET_BLOCKED_DAYS($start: timestamptz, $end: timestamptz) {
|
||||
appointments(
|
||||
where: {
|
||||
_and: [
|
||||
{ block: { _eq: true } }
|
||||
{ canceled: { _eq: false } }
|
||||
{ start: { _gte: $start } }
|
||||
{ end: { _lte: $end } }
|
||||
]
|
||||
}
|
||||
) {
|
||||
id
|
||||
block
|
||||
start
|
||||
end
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||