Compare commits

..

1 Commits

Author SHA1 Message Date
Patrick Fic
bf75ad68f9 Merged master into development 2021-05-20 14:57:30 +00:00
641 changed files with 56239 additions and 73651 deletions

View File

@@ -1 +1 @@
client_max_body_size 50M;
client_max_body_size 15M;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -22,7 +22,7 @@ hooks.js:
module.exports = [
{
path: "/pull",
command: "git pull && yarn && pm2 restart 0",
command: "git pull && npm i",
cwd: "/home/ubuntu/io/",
method: "post",
},

View File

@@ -28,7 +28,7 @@ import JobsShow from "../jobs/jobs.show";
const httpLink = new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
headers: {
"x-hasura-admin-secret": `Dev-BodyShopApp!`,
"x-hasura-admin-secret": `Dev-BodyShopAppBySnaptSoftware!`,
// 'Authorization': `Bearer xxxx`,
},
});
@@ -67,7 +67,7 @@ const client = new ApolloClient({
// uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
// cache: new InMemoryCache(),
// headers: {
// "x-hasura-admin-secret": `Dev-BodyShopApp!`,
// "x-hasura-admin-secret": `Dev-BodyShopAppBySnaptSoftware!`,
// // 'Authorization': `Bearer xxxx`,
// },
// });

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,9 @@
// 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: {
@@ -28,9 +12,7 @@ module.exports = {
modifyVars: {
...(process.env.NODE_ENV === "development"
? { "@primary-color": "#a51d1d" }
: {
//"@primary-color": "#1DA57A"
}),
: { "@primary-color": "#1DA57A" }),
// "@primary-color": " #1890ff", // primary color for all components
// "@link-color": "#1890ff", // link color
// "@success-color": "#52c41a", // success state color
@@ -69,5 +51,4 @@ module.exports = {
},
}),
},
devtool: "source-map",
};

13
client/madge-graph.svg Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 2.44.1 (20200629.0846)
-->
<!-- Title: G Pages: 1 -->
<svg width="43pt" height="43pt"
viewBox="0.00 0.00 43.20 43.20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(21.6 21.6)">
<title>G</title>
<polygon fill="#111111" stroke="transparent" points="-21.6,21.6 -21.6,-21.6 21.6,-21.6 21.6,21.6 -21.6,21.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 613 B

42958
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,92 +4,78 @@
"private": true,
"proxy": "http://localhost:5000",
"dependencies": {
"@apollo/client": "^3.4.16",
"@craco/craco": "^6.3.0",
"@fingerprintjs/fingerprintjs": "^3.3.0",
"@apollo/client": "^3.3.17",
"@craco/craco": "^6.1.2",
"@fingerprintjs/fingerprintjs": "^3.1.2",
"@lourenci/react-kanban": "^2.1.0",
"@openreplay/tracker": "^3.4.4",
"@openreplay/tracker-assist": "^3.4.3",
"@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.19.1",
"@tanem/react-nprogress": "^3.0.79",
"antd": "^4.16.13",
"@sentry/react": "^6.3.6",
"@sentry/tracing": "^6.3.6",
"@stripe/react-stripe-js": "^1.4.0",
"@stripe/stripe-js": "^1.14.0",
"@tanem/react-nprogress": "^3.0.65",
"antd": "^4.15.5",
"apollo-link-logger": "^2.0.0",
"axios": "^0.22.0",
"craco-less": "^1.20.0",
"dinero.js": "^1.9.0",
"dotenv": "^10.0.0",
"enquire-js": "^0.2.1",
"axios": "^0.21.1",
"craco-less": "^1.17.1",
"dinero.js": "^1.8.1",
"dotenv": "^9.0.2",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^9.1.1",
"graphql": "^15.6.0",
"i18next": "^21.2.4",
"i18next-browser-languagedetector": "^6.1.2",
"jsoneditor": "^9.5.6",
"firebase": "^8.6.0",
"graphql": "^15.5.0",
"i18next": "^20.2.2",
"i18next-browser-languagedetector": "^6.1.1",
"jsoneditor": "^9.4.1",
"jsreport-browser-client-dist": "^1.3.0",
"libphonenumber-js": "^1.9.36",
"logrocket": "^2.0.0",
"markerjs2": "^2.13.0",
"libphonenumber-js": "^1.9.17",
"logrocket": "^1.2.0",
"moment-business-days": "^1.2.0",
"phone": "^3.1.8",
"phone": "^2.4.21",
"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",
"query-string": "^7.0.0",
"react": "^17.0.1",
"react-big-calendar": "^0.36.1",
"react-big-calendar": "^0.33.2",
"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.3.0",
"react-i18next": "^11.12.0",
"react-icons": "^4.3.1",
"react-number-format": "^4.7.3",
"react-redux": "^7.2.5",
"react-resizable": "^3.0.4",
"react-router-dom": "^5.3.0",
"react-i18next": "^11.8.15",
"react-icons": "^4.2.0",
"react-number-format": "^4.5.5",
"react-redux": "^7.2.4",
"react-resizable": "^3.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.3",
"react-sublime-video": "^0.2.5",
"react-virtualized": "^9.22.3",
"recharts": "^2.1.4",
"redux": "^4.1.1",
"recharts": "^2.0.7",
"redux": "^4.1.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-state-sync": "^3.1.2",
"reselect": "^4.0.0",
"sass": "^1.42.1",
"socket.io-client": "^4.2.0",
"styled-components": "^5.3.1",
"sass": "^1.32.13",
"styled-components": "^5.3.0",
"subscriptions-transport-ws": "^0.9.18",
"web-vitals": "^2.1.0",
"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"
"web-vitals": "^1.1.2",
"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"
},
"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",
"build:test": "env-cmd -f .env.test yarn run build",
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"build:test": "env-cmd -f .env.test npm run build",
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
"test": "craco test",
"eject": "react-scripts eject",
@@ -114,8 +100,6 @@
]
},
"devDependencies": {
"@sentry/webpack-plugin": "^1.17.2",
"patch-package": "^6.4.7",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.2"
}

File diff suppressed because one or more lines are too long

View File

@@ -8,61 +8,13 @@
<meta name="description" content="ImEX Online" />
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
<link rel="apple-touch-icon" href="logo192.png" />
<script type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
(function () {
d = document;
s = d.createElement("script");
s.src = "https://client.crisp.chat/l.js";
s.async = 1;
d.getElementsByTagName("head")[0].appendChild(s);
})();
</script>
<script>
!(function () {
"use strict";
var e = [
"debug",
"destroy",
"do",
"help",
"identify",
"is",
"off",
"on",
"ready",
"render",
"reset",
"safe",
"set",
];
if (window.noticeable)
console.warn("Noticeable SDK code snippet loaded more than once");
else {
var n = (window.noticeable = window.noticeable || []);
function t(e) {
return function () {
var t = Array.prototype.slice.call(arguments);
return t.unshift(e), n.push(t), n;
};
}
!(function () {
for (var o = 0; o < e.length; o++) {
var r = e[o];
n[r] = t(r);
}
})(),
(function () {
var e = document.createElement("script");
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
var n = document.head;
n.insertBefore(e, n.firstChild);
})();
}
})();
</script>
<!-- <script
data-jsd-embedded
data-key="51adb36e-ee16-46b1-a4c6-4b6d5fcd8530"
data-base-url="https://jsd-widget.atlassian.com"
src="https://jsd-widget.atlassian.com/assets/embed.js"
></script> -->
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View File

@@ -4,41 +4,16 @@ import enLocale from "antd/es/locale/en_US";
import LogRocket from "logrocket";
import moment from "moment";
import React from "react";
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";
import { useTranslation } from "react-i18next";
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();
if (user) tracker.setUserID(user.email);
console.log("ORS SESSION ", sessionID, user && 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() {
const { t } = useTranslation();
return (
<ApolloProvider client={client}>
<ConfigProvider

View File

@@ -1,23 +1,19 @@
import { Button, Result } from "antd";
import React, { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
//Component Imports
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import AboutPage from "../pages/about/about.page";
import TechPageContainer from "../pages/tech/tech.page.container";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
import { selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../utils/private-route";
import "./App.styles.scss";
import LandingPage from "../pages/landing/landing.page";
const LandingPage = lazy(() => import("../pages/landing/landing.page"));
const ResetPassword = lazy(() =>
import("../pages/reset-password/reset-password.component")
);
@@ -31,59 +27,25 @@ const MobilePaymentContainer = lazy(() =>
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
online: selectOnline,
});
const mapDispatchToProps = (dispatch) => ({
checkUserSession: () => dispatch(checkUserSession()),
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
});
export function App({ checkUserSession, currentUser, online, setOnline }) {
export function App({ checkUserSession, currentUser }) {
useEffect(() => {
if (!navigator.onLine) {
setOnline(false);
}
checkUserSession();
}, [checkUserSession, setOnline]);
}, [checkUserSession]);
//const b = Grid.useBreakpoint();
// console.log("Breakpoints:", b);
const { t } = useTranslation();
window.addEventListener("offline", function (e) {
console.log("Internet connection lost.");
setOnline(false);
});
window.addEventListener("online", function (e) {
setOnline(true);
});
if (currentUser.authorized === null) {
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
if (!online)
return (
<Result
status="warning"
title={t("general.labels.nointernet")}
subTitle={t("general.labels.nointernet_sub")}
extra={
<Button
type="primary"
onClick={() => {
window.location.reload();
}}
>
{t("general.actions.refresh")}
</Button>
}
/>
);
return (
<Switch>
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
@@ -100,7 +62,7 @@ export function App({ checkUserSession, currentUser, online, setOnline }) {
<Route exact path="/csi/:surveyId" component={CsiPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route exact path="/disclaimer" component={DisclaimerPage} />
<Route exact path="/about" component={AboutPage} />
</ErrorBoundary>
<ErrorBoundary>
<Route
@@ -123,13 +85,6 @@ export function App({ checkUserSession, currentUser, online, setOnline }) {
component={TechPageContainer}
/>
</ErrorBoundary>
<ErrorBoundary>
<PrivateRoute
isAuthorized={currentUser.authorized}
path="/edit"
component={DocumentEditorContainer}
/>
</ErrorBoundary>
</Suspense>
</Switch>
);

View File

@@ -118,13 +118,3 @@
.production-list-min-height {
min-height: 19px;
}
#noticeable-widget {
iframe {
z-index: 2 !important;
}
}
.react-kanban-column {
background-color: #ddd !important;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 51 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" ?><svg style="enable-background:new 0 0 128 128;" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:8;stroke-miterlimit:10;}
.st1{display:none;}
.st2{display:inline;opacity:0.25;fill:#F45EFD;}
</style><g id="_x31_2_3D_Printing"/><g id="_x31_1_VR_Gear"/><g id="_x31_0_Virtual_reality"/><g id="_x39__Augmented_reality"/><g id="_x38__Teleport"/><g id="_x37__Glassess"/><g id="_x36__Folding_phone"/><g id="_x35__Drone"/><g id="_x34__Retina_scan"/><g id="_x33__Smartwatch"/><g id="_x32__Bionic_Arm"/><g id="_x31__Chip"><g><path d="M108,40c-5.2,0-9.6,3.3-11.3,8H84V32h-8V20h-8v12h-8V20h-8v12h-8v16H24v-8.7c4.7-1.7,8-6.1,8-11.3c0-6.6-5.4-12-12-12 S8,21.4,8,28c0,5.2,3.3,9.6,8,11.3V56h28v8H16v16.7c-4.7,1.7-8,6.1-8,11.3c0,6.6,5.4,12,12,12s12-5.4,12-12c0-5.2-3.3-9.6-8-11.3 V72h20v16h8v12h8V88h8v12h8V88h8V72h8v16.7c-4.7,1.7-8,6.1-8,11.3c0,6.6,5.4,12,12,12s12-5.4,12-12c0-5.2-3.3-9.6-8-11.3V64H84v-8 h12.7c1.7,4.7,6.1,8,11.3,8c6.6,0,12-5.4,12-12S114.6,40,108,40z M20,32c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S22.2,32,20,32z M20,96c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S22.2,96,20,96z M76,80H52V40h24V80z M96,96c2.2,0,4,1.8,4,4s-1.8,4-4,4s-4-1.8-4-4 S93.8,96,96,96z M108,56c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S110.2,56,108,56z"/><rect height="8" width="8" x="56" y="64"/></g></g><g class="st1" id="Guide"><path class="st2" d="M120,8v112H8V8H120 M128,0H0v128h128V0L128,0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,9 +1,25 @@
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>
<QboAuthorizeComponent />
<button onClick={handleQbSignIn}>Sign Into Qb.</button>
</div>
);
}

View File

@@ -9,25 +9,8 @@ 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";
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 }) {
export default function AccountingPayablesTableComponent({ loading, bills }) {
const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -126,17 +109,6 @@ export function AccountingPayablesTableComponent({ bodyshop, 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",
@@ -149,7 +121,6 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
billId={record.id}
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
/>
</div>
),
@@ -183,9 +154,6 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />
)}
<Input
value={state.search}
onChange={handleSearch}

View File

@@ -5,9 +5,9 @@ import { Link } from "react-router-dom";
import { logImEXEvent } from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
import { alphaSort } from "../../utils/sorters";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import { PaymentsExportAllButton } from "../payments-export-all-button/payments-export-all-button.component";
export default function AccountingPayablesTableComponent({
loading,
@@ -41,12 +41,19 @@ export default function AccountingPayablesTableComponent({
title: t("payments.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sorter: (a, b) => alphaSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
},
{
title: t("payments.fields.date"),
dataIndex: "date",
key: "date",
sorter: (a, b) => alphaSort(a.date, b.date),
sortOrder:
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
},
{
title: t("jobs.fields.owner"),
dataIndex: "owner",
@@ -54,7 +61,7 @@ export default function AccountingPayablesTableComponent({
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
state.sortedInfo.columnKey === "ownr_ln" && state.sortedInfo.order,
render: (text, record) => {
return record.job.owner ? (
<Link to={"/manage/owners/" + record.job.owner.id}>
@@ -108,17 +115,7 @@ 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",
@@ -130,7 +127,6 @@ export default function AccountingPayablesTableComponent({
paymentId={record.id}
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
/>
),
},

View File

@@ -8,26 +8,7 @@ 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";
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,
}) {
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -133,28 +114,17 @@ export function AccountingReceivablesTableComponent({
);
},
},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
},
{
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>
@@ -199,17 +169,12 @@ export function AccountingReceivablesTableComponent({
<Card
extra={
<Space wrap>
{!bodyshop.cdk_dealerid && (
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
/>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />
)}
<JobsExportAllButton
jobIds={selectedJobs}
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
/>
<Input.Search
value={state.search}
onChange={handleSearch}

View File

@@ -26,8 +26,6 @@ import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-bu
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -35,8 +33,6 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
export default connect(
@@ -44,10 +40,7 @@ export default connect(
mapDispatchToProps
)(BillDetailEditcontainer);
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
}) {
export function BillDetailEditcontainer({ setPartsOrderContext }) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { t } = useTranslation();
@@ -141,12 +134,6 @@ export function BillDetailEditcontainer({
});
await Promise.all(updates);
insertAuditTrail({
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
await refetch();
form.setFieldsValue(transformData(data));
form.resetFields();

View File

@@ -11,17 +11,13 @@ import {
QUERY_JOB_LBR_ADJUSTMENTS,
UPDATE_JOB,
} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
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({
@@ -31,8 +27,6 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
});
function BillEnterModalContainer({
@@ -40,7 +34,6 @@ function BillEnterModalContainer({
toggleModalVisible,
bodyshop,
currentUser,
insertAuditTrail,
}) {
const [form] = Form.useForm();
const { t } = useTranslation();
@@ -50,31 +43,7 @@ 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;
@@ -112,9 +81,8 @@ function BillEnterModalContainer({
},
],
},
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
});
console.log("adjustmentsToInsert", adjustmentsToInsert);
const adjKeys = Object.keys(adjustmentsToInsert);
if (adjKeys.length > 0) {
//Query the adjustments, merge, and update them.
@@ -147,12 +115,7 @@ function BillEnterModalContainer({
message: JSON.stringify(jobUpdate.errors),
}),
});
return;
}
insertAuditTrail({
jobid: values.jobid,
operation: AuditTrailMapping.jobmodifylbradj(),
});
}
if (!!r1.errors) {
@@ -208,15 +171,9 @@ function BillEnterModalContainer({
});
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
insertAuditTrail({
jobid: values.jobid,
billid: billId,
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
});
if (enterAgain) {
form.resetFields();
form.setFieldsValue(formValues);
form.setFieldsValue({ billlines: [] });
} else {
toggleModalVisible();
}
@@ -234,6 +191,23 @@ 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);

View File

@@ -41,7 +41,6 @@ export function BillFormComponent({
loadLines,
billEdit,
disableInvNumber,
job,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -51,10 +50,6 @@ export function BillFormComponent({
setDiscount(opt.discount);
};
useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
useEffect(() => {
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
const vendorId = form.getFieldValue("vendorid");
@@ -94,7 +89,7 @@ export function BillFormComponent({
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
// notExported={false}
onBlur={() => {
if (form.getFieldValue("jobid") !== null) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
@@ -111,18 +106,6 @@ export function BillFormComponent({
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (
value &&
!getFieldValue(["isinhouse"]) &&
value === bodyshop.inhousevendorid
) {
return Promise.reject(t("bills.validation.manualinhouse"));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
@@ -192,22 +175,6 @@ export function BillFormComponent({
label={t("bills.fields.is_credit_memo")}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
}
return Promise.resolve();
},
}),
]}
>
<Switch />
</Form.Item>

View File

@@ -34,7 +34,6 @@ export function BillFormContainer({
}
loadLines={loadLines}
lineData={lineData ? lineData.joblines : []}
job={lineData ? lineData.jobs_by_pk : null}
responsibilityCenters={bodyshop.md_responsibility_centers || null}
disableInvNumber={disableInvNumber}
/>

View File

@@ -72,11 +72,9 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1,
actual_price: opt.cost,
cost_center: opt.part_type
? responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
? responsibilityCenters.defaults.costs[
opt.part_type
] ||
null)
] || null
: null,
};
}

View File

@@ -1,8 +1,13 @@
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 });
@@ -15,7 +20,8 @@ export const CalculateBillTotal = (invoice) => {
billlines.forEach((i) => {
if (!!i) {
const itemTotal = Dinero({
amount: Math.round((i.actual_cost || 0) * 100),
amount:
Math.round(((i.actual_cost || 0) * 100 + Number.EPSILON) * 100) / 100,
}).multiply(i.quantity || 1);
subtotal = subtotal.add(itemTotal);

View File

@@ -12,19 +12,7 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
disabled={disabled}
ref={ref}
showSearch
// optionFilterProp="line_desc"
filterOption={(inputValue, option) => {
console.log(inputValue);
return (
(option.line_desc &&
option.line_desc
.toLowerCase()
.includes(inputValue.toLowerCase())) ||
(option.oem_partno &&
option.oem_partno.toLowerCase().includes(inputValue.toLowerCase()))
);
}}
notFoundContent={"Removed."}
optionFilterProp="line_desc"
{...restProps}
>
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
@@ -33,19 +21,14 @@ 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}
oem_partno={item.oem_partno}
style={{
...(item.removed ? { textDecoration: "line-through" } : {}),
}}
>
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
{`${item.line_desc}${
item.oem_partno ? ` - ${item.oem_partno}` : ""
}`}
</Option>

View File

@@ -47,9 +47,7 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
});
if (!result.errors) {
notification["success"]({
message: t("bills.successes.reexport"),
});
notification["success"]({ message: t("bills.successes.save") });
} else {
notification["error"]({
message: t("bills.errors.saving", {

View File

@@ -1,11 +1,10 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { EyeFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort } from "../../utils/sorters";
@@ -15,7 +14,6 @@ import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
const mapStateToProps = createStructuredSelector({
//jobRO: selectJobReadOnly,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
@@ -28,7 +26,6 @@ const mapDispatchToProps = (dispatch) => ({
});
export function BillsListTableComponent({
bodyshop,
job,
billsQuery,
handleOnRowClick,
@@ -50,14 +47,12 @@ export function BillsListTableComponent({
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled />
<EyeFilled />
</Button>
)}
<BillDeleteButton bill={record} />
<Button
disabled={
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
}
disabled={record.is_credit_memo}
onClick={() =>
setPartsOrderContext({
actions: {},

View File

@@ -1,11 +1,10 @@
import { HomeFilled } from "@ant-design/icons";
import { Breadcrumb, Row, Col } from "antd";
import { Breadcrumb } from "antd";
import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
import GlobalSearch from "../global-search/global-search.component";
import "./breadcrumbs.styles.scss";
const mapStateToProps = createStructuredSelector({
@@ -14,29 +13,24 @@ const mapStateToProps = createStructuredSelector({
export function BreadCrumbs({ breadcrumbs }) {
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>
item.link ? (
<Breadcrumb.Item key={item.label}>
<Link to={item.link}>{item.label} </Link>
</Breadcrumb.Item>
) : (
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
)
)}
</Breadcrumb>
</Col>
<Col xs={24} sm={24} md={8}>
<GlobalSearch />
</Col>
</Row>
<div className="breadcrumb-container imex-flex-row">
<Breadcrumb separator=">">
<Breadcrumb.Item>
<Link to={`/manage`}>
<HomeFilled />
</Link>
</Breadcrumb.Item>
{breadcrumbs.map((item) =>
item.link ? (
<Breadcrumb.Item key={item.label}>
<Link to={item.link}>{item.label} </Link>
</Breadcrumb.Item>
) : (
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
)
)}
</Breadcrumb>
</div>
);
}
export default connect(mapStateToProps, null)(BreadCrumbs);

View File

@@ -1,15 +1,17 @@
import { useSubscription } from "@apollo/client";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ChatAffixComponent from "./chat-affix.component";
import { Affix } from "antd";
import "./chat-affix.styles.scss";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
chatVisible: selectChatVisible,
@@ -29,20 +31,22 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
return (
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
{bodyshop && bodyshop.messagingservicesid ? (
<ChatAffixComponent
conversationList={(data && data.conversations) || []}
unreadCount={
(data &&
data.conversations.reduce((acc, val) => {
return (acc = acc + val.messages_aggregate.aggregate.count);
}, 0)) ||
0
}
/>
) : null}
</div>
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
<div>
{bodyshop && bodyshop.messagingservicesid ? (
<ChatAffixComponent
conversationList={(data && data.conversations) || []}
unreadCount={
(data &&
data.conversations.reduce((acc, val) => {
return (acc = acc + val.messages_aggregate.aggregate.count);
}, 0)) ||
0
}
/>
) : null}
</div>
</Affix>
);
}
export default connect(mapStateToProps, null)(ChatAffixContainer);

View File

@@ -1,11 +1,6 @@
.chat-affix {
position: fixed;
left: 2vw;
position: absolute;
bottom: 2vh;
z-index: 999;
-webkit-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
-moz-box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
box-shadow: 0px 0px 2px 0px rgba(69, 69, 69, 1);
}
.chat-affix-open {

View File

@@ -1,28 +0,0 @@
import { useMutation } from "@apollo/client";
import { Button } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
export default function ChatArchiveButton({ conversation }) {
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
const handleToggleArchive = async () => {
setLoading(true);
await updateConversation({
variables: { id: conversation.id, archived: !conversation.archived },
});
setLoading(false);
};
return (
<Button onClick={handleToggleArchive} loading={loading} type="primary">
{conversation.archived
? t("messaging.labels.unarchive")
: t("messaging.labels.archive")}
</Button>
);
}

View File

@@ -8,7 +8,6 @@ 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,
@@ -61,18 +60,13 @@ export function ChatConversationListComponent({
) : (
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
)}
<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>
{item.job_conversations.length > 0
? item.job_conversations.map((j, idx) => (
<Tag key={idx} className="ro-number-tag">
{j.job.ro_number}
</Tag>
))
: null}
<Badge count={item.messages_aggregate.aggregate.count || 0} />
</List.Item>
)}

View File

@@ -1,23 +1,24 @@
import { Space } from "antd";
import React from "react";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
export default function ChatConversationTitle({ conversation }) {
return (
<Space wrap>
<PhoneNumberFormatter>
{conversation && conversation.phone_num}
</PhoneNumberFormatter>
<ChatConversationTitleTags
jobConversations={
(conversation && conversation.job_conversations) || []
}
/>
<ChatTagRoContainer conversation={conversation || []} />
<ChatArchiveButton conversation={conversation} />
</Space>
<div>
<div className="imex-flex-row">
<ChatConversationTitleTags
jobConversations={
(conversation && conversation.job_conversations) || []
}
/>
<ChatTagRoContainer conversation={conversation || []} />
</div>
<div className="imex-flex-row">
<PhoneNumberFormatter>
{conversation && conversation.phone_num}
</PhoneNumberFormatter>
</div>
</div>
);
}

View File

@@ -1,5 +1,4 @@
import Icon from "@ant-design/icons";
import { Tooltip } from "antd";
import i18n from "i18next";
import moment from "moment";
import React, { useEffect, useRef } from "react";
@@ -10,7 +9,6 @@ import {
CellMeasurerCache,
List,
} from "react-virtualized";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import "./chat-message-list.styles.scss";
export default function ChatMessageListComponent({ messages }) {
@@ -87,22 +85,17 @@ export default function ChatMessageListComponent({ messages }) {
const MessageRender = (message) => {
return (
<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>
<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>
);
};

View File

@@ -1,5 +1,5 @@
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
import { Select, Empty, Space } from "antd";
import { Select, Empty } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -13,27 +13,27 @@ export default function ChatTagRoComponent({
const { t } = useTranslation();
return (
<Space flex>
<div style={{ width: "15rem" }}>
<Select
showSearch
autoFocus
dropdownMatchSelectWidth
placeholder={t("general.labels.search")}
filterOption={false}
onSearch={handleSearch}
onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
>
{roOptions.map((item, idx) => (
<Select.Option key={item.id || idx}>
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
item.ownr_ln || ""
} ${item.ownr_co_nm || ""}`}
</Select.Option>
))}
</Select>
</div>
<div>
<Select
showSearch
autoFocus
style={{
width: 300,
}}
placeholder={t("general.labels.search")}
filterOption={false}
onSearch={handleSearch}
onSelect={handleInsertTag}
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
>
{roOptions.map((item, idx) => (
<Select.Option key={item.id || idx}>
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
item.ownr_ln || ""
} ${item.ownr_co_nm || ""}`}
</Select.Option>
))}
</Select>
{loading ? <LoadingOutlined /> : null}
{loading ? (
@@ -41,6 +41,6 @@ export default function ChatTagRoComponent({
) : (
<CloseCircleOutlined onClick={() => setVisible(false)} />
)}
</Space>
</div>
);
}

View File

@@ -17,7 +17,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
return (
<div>
<Form.Item name="plate" label={t("courtesycars.fields.plate")}>
<Form.Item name="fleet" label={t("courtesycars.fields.fleetnumber")}>
<Input />
</Form.Item>
<Form.Item

View File

@@ -44,8 +44,8 @@ export function ContractsFindModalContainer({
callSearch({
variables: {
plate:
(values.plate && values.plate !== "" && values.plate) || undefined,
fleet:
(values.fleet && values.fleet !== "" && values.fleet) || undefined,
time: values.time,
},
});

View File

@@ -83,10 +83,8 @@ 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.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""
} ${record.courtesycar.make} ${record.courtesycar.model} ${
record.courtesycar.plate ? `(${record.courtesycar.plate})` : ""
}`}</Link>
),
},

View File

@@ -31,9 +31,6 @@ 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>
);
};

View File

@@ -45,10 +45,6 @@ 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:
@@ -94,12 +90,13 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
// sorter: (a, b) => alphaSort(a.model, b.model),
sortOrder:
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
render: (text, record) =>
record.cccontracts.length === 1 ? (
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
{record.cccontracts[0].job.ro_number}
</Link>
) : null,
render: (text, record) => (
<div>
{record.cccontracts.length === 1
? record.cccontracts[0].job.ro_number
: null}
</div>
),
},
];

View File

@@ -1,166 +0,0 @@
import { Card } from "antd";
import _ from "lodash";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import {
Bar,
CartesianGrid,
ComposedChart,
Legend,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyEmployeeEfficiency({
data,
...cardProps
}) {
const { t } = useTranslation();
if (!data) return null;
if (!data.monthly_employee_efficiency)
return <DashboardRefreshRequired {...cardProps} />;
const ticketsByDate = _.groupBy(data.monthly_employee_efficiency, (item) =>
moment(item.date).format("YYYY-MM-DD")
);
const listOfDays = Utils.ListOfDaysInCurrentMonth();
const chartData = listOfDays.reduce((acc, val) => {
//Sum up the current day.
let dailyHrs;
if (!!ticketsByDate[val]) {
dailyHrs = ticketsByDate[val].reduce(
(dayAcc, dayVal) => {
return {
actual: dayAcc.actual + dayVal.actualhrs,
productive: dayAcc.actual + dayVal.productivehrs,
};
},
{ actual: 0, productive: 0 }
);
} else {
dailyHrs = { actual: 0, productive: 0 };
}
const dailyEfficiency =
((dailyHrs.productive - dailyHrs.actual) / dailyHrs.productive + 1) * 100;
const theValue = {
date: moment(val).format("DD"),
...dailyHrs,
dailyEfficiency: isNaN(dailyEfficiency) ? 0 : dailyEfficiency.toFixed(1),
accActual:
acc.length > 0
? acc[acc.length - 1].accActual + dailyHrs.actual
: dailyHrs.actual,
accProductive:
acc.length > 0
? acc[acc.length - 1].accProductive + dailyHrs.productive
: dailyHrs.productive,
accEfficiency: 0,
};
theValue.accEfficiency = (
((theValue.accProductive - theValue.accActual) /
(theValue.accProductive || 1) +
1) *
100
).toFixed(1);
return [...acc, theValue];
}, []);
return (
<Card
title={t("dashboard.titles.monthlyemployeeefficiency")}
{...cardProps}
>
<div style={{ height: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={chartData}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" />
<YAxis
yAxisId="left"
orientation="left"
stroke="#8884d8"
unit=" hrs"
/>
<YAxis
yAxisId="right"
orientation="right"
stroke="#82ca9d"
unit="%"
/>
<Tooltip />
<Legend />
<Line
yAxisId="right"
name="Accumulated Efficiency"
type="monotone"
unit="%"
dataKey="accEfficiency"
stroke="#152228"
connectNulls
// activeDot={{ r: 8 }}
/>
<Line
name="Daily Efficiency"
yAxisId="right"
unit="%"
type="monotone"
connectNulls
dataKey="dailyEfficiency"
stroke="#d31717"
/>
<Bar
name="Actual Hours"
dataKey="actual"
yAxisId="left"
unit=" hrs"
//stackId="day"
barSize={20}
fill="#102568"
/>
<Bar
name="Productive Hours"
dataKey="productive"
yAxisId="left"
unit=" hrs"
//stackId="day"
barSize={20}
fill="#017664"
/>
</ComposedChart>
</ResponsiveContainer>
</div>
</Card>
);
}
export const DashboardMonthlyEmployeeEfficiencyGql = `
monthly_employee_efficiency: timetickets(where: {_and: [{date: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}},{date: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}} ]}) {
actualhrs
productivehrs
employeeid
employee {
first_name
last_name
}
date
}
`;

View File

@@ -1,163 +0,0 @@
import { Card, Input, Space, Table, Typography } from "antd";
import axios from "axios";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../../utils/sorters";
import LoadingSkeleton from "../../loading-skeleton/loading-skeleton.component";
import Dinero from "dinero.js";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyJobCosting({ data, ...cardProps }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [costingData, setcostingData] = useState(null);
const [searchText, setSearchText] = useState("");
const [state, setState] = useState({
sortedInfo: {},
});
useEffect(() => {
async function getCostingData() {
if (data && data.monthly_sales) {
setLoading(true);
const response = await axios.post("/job/costingmulti", {
jobids: data.monthly_sales.map((x) => x.id),
});
setcostingData(response.data);
setLoading(false);
}
}
getCostingData();
}, [data]);
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const columns = [
{
title: t("bodyshop.fields.responsibilitycenter"),
dataIndex: "cost_center",
key: "cost_center",
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
sortOrder:
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
},
{
title: t("jobs.labels.sales"),
dataIndex: "sales",
key: "sales",
sorter: (a, b) =>
parseFloat(a.sales.substring(1)) - parseFloat(b.sales.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "sales" && state.sortedInfo.order,
},
{
title: t("jobs.labels.costs"),
dataIndex: "costs",
key: "costs",
sorter: (a, b) =>
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
},
{
title: t("jobs.labels.gpdollars"),
dataIndex: "gpdollars",
key: "gpdollars",
sorter: (a, b) =>
parseFloat(a.gpdollars.substring(1)) -
parseFloat(b.gpdollars.substring(1)),
sortOrder:
state.sortedInfo.columnKey === "gpdollars" && state.sortedInfo.order,
},
{
title: t("jobs.labels.gppercent"),
dataIndex: "gppercent",
key: "gppercent",
sorter: (a, b) =>
parseFloat(a.gppercent.slice(0, -1) || 0) -
parseFloat(b.gppercent.slice(0, -1) || 0),
sortOrder:
state.sortedInfo.columnKey === "gppercent" && state.sortedInfo.order,
},
];
const filteredData =
searchText === ""
? (costingData && costingData.allCostCenterData) || []
: costingData.allCostCenterData.filter((d) =>
(d.cost_center || "")
.toString()
.toLowerCase()
.includes(searchText.toLowerCase())
);
return (
<Card
title={t("dashboard.titles.monthlyjobcosting")}
extra={
<Space wrap>
<Input.Search
placeholder={t("general.labels.search")}
value={searchText}
onChange={(e) => {
e.preventDefault();
setSearchText(e.target.value);
}}
/>
</Space>
}
{...cardProps}
>
<LoadingSkeleton loading={loading}>
<div style={{ height: "100%" }}>
<Table
onChange={handleTableChange}
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
scroll={{ x: true, y: "calc(100% - 4em)" }}
rowKey="id"
style={{ height: "100%" }}
dataSource={filteredData}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell>
<Typography.Title level={4}>
{t("general.labels.totals")}
</Typography.Title>
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(
costingData &&
costingData.allSummaryData &&
costingData.allSummaryData.totalSales
).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(
costingData &&
costingData.allSummaryData &&
costingData.allSummaryData.totalCost
).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell>
{Dinero(
costingData &&
costingData.allSummaryData &&
costingData.allSummaryData.gpdollars
).toFormat()}
</Table.Summary.Cell>
<Table.Summary.Cell></Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>
</LoadingSkeleton>
</Card>
);
}

View File

@@ -1,163 +0,0 @@
import { Card } from "antd";
import Dinero from "dinero.js";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyLaborSales({ data, ...cardProps }) {
const { t } = useTranslation();
const [activeIndex, setActiveIndex] = useState(0);
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const laborData = {};
data.monthly_sales.forEach((job) => {
job.joblines.forEach((jobline) => {
if (!jobline.mod_lbr_ty) return;
if (!laborData[jobline.mod_lbr_ty])
laborData[jobline.mod_lbr_ty] = Dinero();
laborData[jobline.mod_lbr_ty] = laborData[jobline.mod_lbr_ty].add(
Dinero({
amount: Math.round(
(job[`rate_${jobline.mod_lbr_ty.toLowerCase()}`] || 0) * 100
),
}).multiply(jobline.mod_lb_hrs || 0)
);
});
});
const chartData = Object.keys(laborData).map((key) => {
return {
name: t(`joblines.fields.lbr_types.${key.toUpperCase()}`),
value: laborData[key].getAmount() / 100,
color: pieColor(key.toUpperCase()),
};
});
return (
<Card title={t("dashboard.titles.monthlylaborsales")} {...cardProps}>
<div style={{ height: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart margin={0} padding={0}>
<Pie
data={chartData}
activeIndex={activeIndex}
activeShape={renderActiveShape}
cx="50%"
cy="50%"
innerRadius="60%"
// outerRadius={80}
fill="#8884d8"
dataKey="value"
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
</Card>
);
}
export const DashboardMonthlyRevenueGraphGql = `
`;
const pieColor = (type) => {
if (type === "LAA") return "lightgreen";
else if (type === "LAB") return "dodgerblue";
else if (type === "LAD") return "aliceblue";
else if (type === "LAE") return "seafoam";
else if (type === "LAG") return "chartreuse";
else if (type === "LAF") return "magenta";
else if (type === "LAM") return "gold";
else if (type === "LAR") return "crimson";
else if (type === "LAU") return "slategray";
else if (type === "LA1") return "slategray";
else if (type === "LA2") return "slategray";
else if (type === "LA3") return "slategray";
else if (type === "LA4") return "slategray";
return "slategray";
};
const renderActiveShape = (props) => {
//const RADIAN = Math.PI / 180;
const {
cx,
cy,
//midAngle,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
// percent,
value,
} = props;
// const sin = Math.sin(-RADIAN * midAngle);
// const cos = Math.cos(-RADIAN * midAngle);
// // const sx = cx + (outerRadius + 10) * cos;
// const sy = cy + (outerRadius + 10) * sin;
// const mx = cx + (outerRadius + 30) * cos;
// const my = cy + (outerRadius + 30) * sin;
// //const ex = mx + (cos >= 0 ? 1 : -1) * 22;
// const ey = my;
//const textAnchor = cos >= 0 ? "start" : "end";
return (
<g>
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
{payload.name}
</text>
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={fill}
/>
</g>
);
};
// <path
// d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`}
// stroke={fill}
// fill="none"
// />;
// <text
// x={ex + (cos >= 0 ? 1 : -1) * 12}
// y={ey}
// textAnchor={textAnchor}
// fill="#333"
// >
// {payload.name}
// </text>
// <text
// x={ex + (cos >= 0 ? 1 : -1) * 12}
// y={ey}
// dy={18}
// textAnchor={textAnchor}
// fill="#999"
// >
// {Dinero({ amount: Math.round(value * 100) }).toFormat()}
// </text>

View File

@@ -1,136 +0,0 @@
import { Card } from "antd";
import Dinero from "dinero.js";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Cell, Pie, PieChart, ResponsiveContainer, Sector } from "recharts";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyPartsSales({ data, ...cardProps }) {
const { t } = useTranslation();
const [activeIndex, setActiveIndex] = useState(0);
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const partData = {};
data.monthly_sales.forEach((job) => {
job.joblines.forEach((jobline) => {
if (!jobline.part_type) return;
if (!partData[jobline.part_type]) partData[jobline.part_type] = Dinero();
partData[jobline.part_type] = partData[jobline.part_type].add(
Dinero({ amount: Math.round((jobline.act_price || 0) * 100) }).multiply(
jobline.part_qty || 0
)
);
});
});
const chartData = Object.keys(partData).map((key) => {
return {
name: t(`joblines.fields.part_types.${key.toUpperCase()}`),
value: partData[key].getAmount() / 100,
color: pieColor(key.toUpperCase()),
};
});
return (
<Card title={t("dashboard.titles.monthlypartssales")} {...cardProps}>
<div style={{ height: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<PieChart margin={0} padding={0}>
<Pie
data={chartData}
activeIndex={activeIndex}
activeShape={renderActiveShape}
cx="50%"
cy="50%"
innerRadius="60%"
// outerRadius={80}
fill="#8884d8"
dataKey="value"
onMouseEnter={(throwaway, index) => setActiveIndex(index)}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
</Card>
);
}
export const DashboardMonthlyRevenueGraphGql = `
`;
const pieColor = (type) => {
if (type === "PAA") return "darkgreen";
else if (type === "PAC") return "green";
else if (type === "PAE") return "gold";
else if (type === "PAG") return "seafoam";
else if (type === "PAL") return "chartreuse";
else if (type === "PAM") return "magenta";
else if (type === "PAN") return "crimson";
else if (type === "PAO") return "gold";
else if (type === "PAP") return "crimson";
else if (type === "PAR") return "indigo";
else if (type === "PAS") return "dodgerblue";
else if (type === "PASL") return "dodgerblue";
return "slategray";
};
const renderActiveShape = (props) => {
// const RADIAN = Math.PI / 180;
const {
cx,
cy,
// midAngle,
innerRadius,
outerRadius,
startAngle,
endAngle,
fill,
payload,
// percent,
value,
} = props;
// const sin = Math.sin(-RADIAN * midAngle);
// const cos = Math.cos(-RADIAN * midAngle);
// const sx = cx + (outerRadius + 10) * cos;
//const sy = cy + (outerRadius + 10) * sin;
// const mx = cx + (outerRadius + 30) * cos;
//const my = cy + (outerRadius + 30) * sin;
// const ex = mx + (cos >= 0 ? 1 : -1) * 22;
// const ey = my;
// const textAnchor = cos >= 0 ? "start" : "end";
return (
<g>
<text x={cx} y={cy} dy={0} textAnchor="middle" fill={fill}>
{payload.name}
</text>
<text x={cx} y={cy} dy={16} textAnchor="middle" fill={fill}>
{Dinero({ amount: Math.round(value * 100) }).toFormat()}
</text>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={fill}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={fill}
/>
</g>
);
};

View File

@@ -2,30 +2,29 @@ import { Card } from "antd";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import _ from "lodash";
import {
Area,
Bar,
CartesianGrid,
ComposedChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
Area,
Bar,
CartesianGrid,
ComposedChart,
Legend,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis
} from "recharts";
import Dinero from "dinero.js";
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
const { t } = useTranslation();
if (!data) return null;
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
moment(item.date_invoiced).format("YYYY-MM-DD")
);
const jobsByDate = {
"2020-07-5": [{ clm_total: 1224 }],
"2020-07-8": [{ clm_total: 987 }, { clm_total: 8755 }],
"2020-07-12": [{ clm_total: 684 }, { clm_total: 12022 }],
"2020-07-21": [{ clm_total: 15000 }],
"2020-07-28": [{ clm_total: 122 }, { clm_total: 4522 }],
};
const listOfDays = Utils.ListOfDaysInCurrentMonth();
@@ -34,21 +33,17 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
let dailySales;
if (!!jobsByDate[val]) {
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
return dayAcc.add(
Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0)
);
}, Dinero());
return dayAcc + dayVal.clm_total;
}, 0);
} else {
dailySales = Dinero();
dailySales = 0;
}
const theValue = {
date: moment(val).format("DD"),
dailySales: dailySales.getAmount() / 100,
date: moment(val).format("D dd"),
dailySales,
accSales:
acc.length > 0
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
: dailySales.getAmount() / 100,
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales,
};
return [...acc, theValue];
@@ -56,40 +51,32 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
return (
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
<div style={{ height: "100%" }}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={chartData}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip
formatter={(value, name, props) => value && value.toFixed(2)}
/>
<Legend />
<Area
type="monotone"
name="Accumulated Sales"
dataKey="accSales"
fill="#3CB371"
stroke="#3CB371"
/>
<Bar
name="Daily Sales"
dataKey="dailySales"
//stackId="day"
barSize={20}
fill="#413ea0"
/>
</ComposedChart>
</ResponsiveContainer>
</div>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
data={chartData}
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
>
<CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
<Area
type="monotone"
name="Accumulated Sales"
dataKey="accSales"
fill="#8884d8"
stroke="#8884d8"
/>
<Bar
name="Daily Sales"
dataKey="dailySales"
//stackId="day"
barSize={20}
fill="#413ea0"
/>
</ComposedChart>
</ResponsiveContainer>
</Card>
);
}
export const DashboardMonthlyRevenueGraphGql = `
`;

View File

@@ -1,47 +1,30 @@
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import moment from "moment";
import React from "react";
import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
const { t } = useTranslation();
if (!data) return null;
if (!data.projected_monthly_sales)
return <DashboardRefreshRequired {...cardProps} />;
const aboveTargetMonthlySales = false;
const dollars =
data.projected_monthly_sales &&
data.projected_monthly_sales.reduce(
(acc, val) =>
acc.add(
Dinero(
val.job_totals &&
val.job_totals.totals &&
val.job_totals.totals.subtotal
)
),
Dinero()
);
return (
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
<Statistic value={dollars.toFormat()} />
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.projectedmonthlysales")}
value={222000.0}
precision={2}
prefix={
<div>
{aboveTargetMonthlySales ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
/>
</Card>
);
}
export const DashboardProjectedMonthlySalesGql = `
projected_monthly_sales: jobs(where: {_or: [{_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}, {_and: [{scheduled_completion: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {scheduled_completion: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}]}) {
id
date_invoiced
job_totals
}
`;

View File

@@ -1,25 +0,0 @@
import { SyncOutlined } from "@ant-design/icons";
import { Card } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
export default function DashboardRefreshRequired(props) {
const { t } = useTranslation();
return (
<Card {...props}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
textOverflow: "ellipsis",
}}
>
<SyncOutlined style={{ fontSize: "300%", margin: "1rem" }} />
<div>{t("dashboard.errors.refreshrequired")}</div>
</div>
</Card>
);
}

View File

@@ -1,27 +1,33 @@
import { Card, Statistic } from "antd";
import Dinero from "dinero.js";
import React from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import DashboardRefreshRequired from "../refresh-required.component";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
export default function DashboardTotalProductionDollars({
data,
...cardProps
}) {
const { t } = useTranslation();
if (!data) return null;
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
const dollars =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) =>
acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)),
Dinero()
);
const aboveTargetProductionDollars = false;
return (
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
<Statistic value={dollars.toFormat()} />
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.productiondollars")}
value={175000.0}
precision={2}
prefix={
<div>
{aboveTargetProductionDollars ? (
<ArrowUpOutlined />
) : (
<ArrowDownOutlined />
)}
$
</div>
}
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
/>
</Card>
);
}

View File

@@ -1,63 +1,19 @@
import { Card, Space, Statistic } from "antd";
import React from "react";
import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../../redux/user/user.selectors";
import DashboardRefreshRequired from "../refresh-required.component";
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DashboardTotalProductionHours);
export function DashboardTotalProductionHours({
bodyshop,
data,
...cardProps
}) {
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
const { t } = useTranslation();
if (!data) return null;
if (!data.production_jobs) return <DashboardRefreshRequired {...cardProps} />;
const hours =
data.production_jobs &&
data.production_jobs.reduce(
(acc, val) => {
return {
body: acc.body + val.labhrs.aggregate.sum.mod_lb_hrs,
ref: acc.ref + val.larhrs.aggregate.sum.mod_lb_hrs,
total:
acc.total +
val.labhrs.aggregate.sum.mod_lb_hrs +
val.larhrs.aggregate.sum.mod_lb_hrs,
};
},
{ body: 0, ref: 0, total: 0 }
);
const aboveTargetHours = hours.total >= bodyshop.prodtargethrs;
const aboveTargetHours = true;
return (
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
<Space wrap style={{ flex: 1 }}>
<Statistic
title={t("dashboard.labels.bodyhrs")}
value={hours.body.toFixed(1)}
/>
<Statistic
title={t("dashboard.labels.refhrs")}
value={hours.ref.toFixed(1)}
/>
<Statistic
title={t("dashboard.labels.prodhrs")}
value={hours.total.toFixed(1)}
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
/>
</Space>
<Card {...cardProps}>
<Statistic
title={t("dashboard.titles.productionhours")}
value={750}
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
/>
</Card>
);
}
export const DashboardTotalProductionHoursGql = ``;

View File

@@ -1,355 +1,185 @@
import Icon, { SyncOutlined } from "@ant-design/icons";
import { gql, useMutation, useQuery } from "@apollo/client";
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
import i18next from "i18next";
import _ from "lodash";
import moment from "moment";
import React, { useState } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { useTranslation } from "react-i18next";
import { MdClose } from "react-icons/md";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import DashboardMonthlyEmployeeEfficiency, {
DashboardMonthlyEmployeeEfficiencyGql,
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
import DashboardMonthlyRevenueGraph, {
DashboardMonthlyRevenueGraphGql,
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
import DashboardProjectedMonthlySales, {
DashboardProjectedMonthlySalesGql,
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
import DashboardTotalProductionHours, {
DashboardTotalProductionHoursGql,
} from "../dashboard-components/total-production-hours/total-production-hours.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
//Combination of the following:
// /node_modules/react-grid-layout/css/styles.css
// /node_modules/react-resizable/css/styles.css
import "./dashboard-grid.styles.scss";
import { GenerateDashboardData } from "./dashboard-grid.utils";
// import Icon from "@ant-design/icons";
// import { Button, Dropdown, Menu, notification } from "antd";
// import React, { useState } from "react";
// import { useMutation, useQuery } from "@apollo/client";
// import { Responsive, WidthProvider } from "react-grid-layout";
// import { useTranslation } from "react-i18next";
// import { MdClose } from "react-icons/md";
// import { connect } from "react-redux";
// import { createStructuredSelector } from "reselect";
// import { logImEXEvent } from "../../firebase/firebase.utils";
// import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries";
// import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
// import {
// selectBodyshop,
// selectCurrentUser,
// } from "../../redux/user/user.selectors";
// import AlertComponent from "../alert/alert.component";
// import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
// import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
// import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
// import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component";
// import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
// //Combination of the following:
// // /node_modules/react-grid-layout/css/styles.css
// // /node_modules/react-resizable/css/styles.css
// import "./dashboard-grid.styles.css";
// import "./dashboard-grid.styles.scss";
const ResponsiveReactGridLayout = WidthProvider(Responsive);
// const ResponsiveReactGridLayout = WidthProvider(Responsive);
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
// const mapStateToProps = createStructuredSelector({
// currentUser: selectCurrentUser,
// bodyshop: selectBodyshop,
// });
// const mapDispatchToProps = (dispatch) => ({
// //setUserLanguage: language => dispatch(setUserLanguage(language))
// });
export function DashboardGridComponent({ currentUser, bodyshop }) {
const { t } = useTranslation();
const [state, setState] = useState({
...(bodyshop.associations[0].user.dashboardlayout
? bodyshop.associations[0].user.dashboardlayout
: { items: [], layout: {}, layouts: [] }),
});
// export function DashboardGridComponent({ currentUser, bodyshop }) {
// const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS);
// const { t } = useTranslation();
// const [state, setState] = useState({
// layout: bodyshop.associations[0].user.dashboardlayout || [
// { i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 },
// // { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 },
// ],
// });
// const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
const { loading, error, data, refetch } = useQuery(
createDashboardQuery(state)
);
// const handleLayoutChange = async (newLayout) => {
// logImEXEvent("dashboard_change_layout");
// setState({ ...state, layout: newLayout });
// const result = await updateLayout({
// variables: { email: currentUser.email, layout: newLayout },
// });
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
// if (!!result.errors) {
// notification["error"]({
// message: t("dashboard.errors.updatinglayout", {
// message: JSON.stringify(result.errors),
// }),
// });
// }
// };
const handleLayoutChange = async (layout, layouts) => {
logImEXEvent("dashboard_change_layout");
// const handleRemoveComponent = (key) => {
// logImEXEvent("dashboard_remove_component", { name: key });
setState({ ...state, layout, layouts });
// const idxToRemove = state.layout.findIndex((i) => i.i === key);
// const newLayout = state.layout;
// newLayout.splice(idxToRemove, 1);
// handleLayoutChange(newLayout);
// };
const result = await updateLayout({
variables: {
email: currentUser.email,
layout: { ...state, layout, layouts },
},
});
if (!!result.errors) {
notification["error"]({
message: t("dashboard.errors.updatinglayout", {
message: JSON.stringify(result.errors),
}),
});
}
};
const handleRemoveComponent = (key) => {
logImEXEvent("dashboard_remove_component", { name: key });
const idxToRemove = state.items.findIndex((i) => i.i === key);
console.log(
"🚀 ~ file: dashboard-grid.component.jsx ~ line 81 ~ idxToRemove",
idxToRemove
);
const items = _.cloneDeep(state.items);
// const handleAddComponent = (e) => {
// logImEXEvent("dashboard_add_component", { name: e });
items.splice(idxToRemove, 1);
setState({ ...state, items });
};
// handleLayoutChange([
// ...state.layout,
// {
// i: e.key,
// x: (state.layout.length * 2) % (state.cols || 12),
// y: Infinity, // puts it at the bottom
// w: componentList[e.key].w || 2,
// h: componentList[e.key].h || 2,
// },
// ]);
// };
const handleAddComponent = (e) => {
logImEXEvent("dashboard_add_component", { name: e });
setState({
...state,
items: [
...state.items,
{
i: e.key,
x: (state.items.length * 2) % (state.cols || 12),
y: 99, // puts it at the bottom
w: componentList[e.key].w || 2,
h: componentList[e.key].h || 2,
},
],
});
};
// const onBreakpointChange = (breakpoint, cols) => {
// setState({ ...state, breakpoint: breakpoint, cols: cols });
// };
const dashboarddata = React.useMemo(
() => GenerateDashboardData(data),
[data]
);
const existingLayoutKeys = state.items.map((i) => i.i);
const addComponentOverlay = (
<Menu onClick={handleAddComponent}>
{Object.keys(componentList).map((key) => (
<Menu.Item
key={key}
value={key}
disabled={existingLayoutKeys.includes(key)}
>
{componentList[key].label}
</Menu.Item>
))}
</Menu>
);
// const existingLayoutKeys = state.layout.map((i) => i.i);
// const addComponentOverlay = (
// <Menu onClick={handleAddComponent}>
// {Object.keys(componentList).map((key) => (
// <Menu.Item
// key={key}
// value={key}
// disabled={existingLayoutKeys.includes(key)}
// >
// {componentList[key].label}
// </Menu.Item>
// ))}
// </Menu>
// );
if (error) return <AlertComponent message={error.message} type="error" />;
// if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
<PageHeader
extra={
<Space>
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
<Button>{t("dashboard.actions.addcomponent")}</Button>
</Dropdown>
</Space>
}
/>
// return (
// <div>
// <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
// <Button>{t("dashboard.actions.addcomponent")}</Button>
// </Dropdown>
// <ResponsiveReactGridLayout
// className="layout"
// breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
// cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
// width="100%"
// onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
// >
// {state.layout.map((item, index) => {
// const TheComponent = componentList[item.i].component;
// return (
// <div key={item.i} data-grid={item}>
// <LoadingSkeleton loading={loading}>
// <Icon
// component={MdClose}
// key={item.i}
// style={{
// position: "absolute",
// zIndex: "2",
// right: ".25rem",
// top: ".25rem",
// cursor: "pointer",
// }}
// onClick={() => handleRemoveComponent(item.i)}
// />
// <TheComponent
// className="dashboard-card"
// size="small"
// style={{ height: "100%", width: "100%" }}
// />
// </LoadingSkeleton>
// </div>
// );
// })}
// </ResponsiveReactGridLayout>
// </div>
// );
// }
<ResponsiveReactGridLayout
className="layout"
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
width="100%"
layouts={state.layouts}
onLayoutChange={handleLayoutChange}
// onBreakpointChange={onBreakpointChange}
>
{state.items.map((item, index) => {
const TheComponent = componentList[item.i].component;
return (
<div
key={item.i}
data-grid={{
...item,
minH: componentList[item.i].minH || 1,
minW: componentList[item.i].minW || 1,
}}
>
<LoadingSkeleton loading={loading}>
<Icon
component={MdClose}
key={item.i}
style={{
position: "absolute",
zIndex: "2",
right: ".25rem",
top: ".25rem",
cursor: "pointer",
}}
onClick={() => handleRemoveComponent(item.i)}
/>
<TheComponent className="dashboard-card" data={dashboarddata} />
</LoadingSkeleton>
</div>
);
})}
</ResponsiveReactGridLayout>
</div>
);
}
// export default connect(
// mapStateToProps,
// mapDispatchToProps
// )(DashboardGridComponent);
export default connect(
mapStateToProps,
mapDispatchToProps
)(DashboardGridComponent);
const componentList = {
ProductionDollars: {
label: i18next.t("dashboard.titles.productiondollars"),
component: DashboardTotalProductionDollars,
gqlFragment: null,
w: 1,
h: 1,
minW: 2,
minH: 1,
},
ProductionHours: {
label: i18next.t("dashboard.titles.productionhours"),
component: DashboardTotalProductionHours,
gqlFragment: DashboardTotalProductionHoursGql,
w: 3,
h: 1,
minW: 3,
minH: 1,
},
ProjectedMonthlySales: {
label: i18next.t("dashboard.titles.projectedmonthlysales"),
component: DashboardProjectedMonthlySales,
gqlFragment: DashboardProjectedMonthlySalesGql,
w: 2,
h: 1,
minW: 2,
minH: 1,
},
MonthlyRevenueGraph: {
label: i18next.t("dashboard.titles.monthlyrevenuegraph"),
component: DashboardMonthlyRevenueGraph,
gqlFragment: DashboardMonthlyRevenueGraphGql,
w: 4,
h: 2,
minW: 4,
minH: 2,
},
MonthlyJobCosting: {
label: i18next.t("dashboard.titles.monthlyjobcosting"),
component: DashboardMonthlyJobCosting,
gqlFragment: null,
minW: 6,
minH: 3,
w: 6,
h: 3,
},
MonthlyPartsSales: {
label: i18next.t("dashboard.titles.monthlypartssales"),
component: DashboardMonthlyPartsSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyLaborSales: {
label: i18next.t("dashboard.titles.monthlylaborsales"),
component: DashboardMonthlyLaborSales,
gqlFragment: null,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
MonthlyEmployeeEfficency: {
label: i18next.t("dashboard.titles.monthlyemployeeefficiency"),
component: DashboardMonthlyEmployeeEfficiency,
gqlFragment: DashboardMonthlyEmployeeEfficiencyGql,
minW: 2,
minH: 2,
w: 2,
h: 2,
},
};
const createDashboardQuery = (state) => {
const componentBasedAdditions =
state &&
Array.isArray(state.layout) &&
state.layout
.map((item, index) => componentList[item.i].gqlFragment || "")
.join("");
return gql`
query QUERY_DASHBOARD_DETAILS {
${componentBasedAdditions || ""}
monthly_sales: jobs(where: {_and: [{date_invoiced: {_gte: "${moment()
.startOf("month")
.format("YYYY-MM-DD")}"}}, {date_invoiced: {_lte: "${moment()
.endOf("month")
.format("YYYY-MM-DD")}"}}]}) {
id
date_invoiced
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
}
production_jobs: jobs(where: { inproduction: { _eq: true } }) {
id
ro_number
ins_co_nm
job_totals
joblines(where: { removed: { _eq: false } }) {
id
mod_lbr_ty
mod_lb_hrs
act_price
part_qty
part_type
}
labhrs: joblines_aggregate(where: { mod_lbr_ty: { _neq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
larhrs: joblines_aggregate(where: { mod_lbr_ty: { _eq: "LAR" } }) {
aggregate {
sum {
mod_lb_hrs
}
}
}
}
}
`;
};
// const componentList = {
// ProductionDollars: {
// label: "Production Dollars",
// component: DashboardTotalProductionDollars,
// w: 2,
// h: 1,
// },
// ProductionHours: {
// label: "Production Hours",
// component: DashboardTotalProductionHours,
// w: 2,
// h: 1,
// },
// ProjectedMonthlySales: {
// label: "Projected Monthly Sales",
// component: DashboardProjectedMonthlySales,
// w: 2,
// h: 1,
// },
// MonthlyRevenueGraph: {
// label: "Monthly Sales Graph",
// component: DashboardMonthlyRevenueGraph,
// w: 2,
// h: 2,
// },
// };

View File

@@ -0,0 +1,128 @@
.react-resizable {
position: relative;
}
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
background-position: bottom right;
padding: 0 3px 3px 0;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
transform: rotate(90deg);
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
transform: rotate(180deg);
}
.react-resizable-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
transform: rotate(270deg);
}
.react-resizable-handle-w,
.react-resizable-handle-e {
top: 50%;
margin-top: -10px;
cursor: ew-resize;
}
.react-resizable-handle-w {
left: 0;
transform: rotate(135deg);
}
.react-resizable-handle-e {
right: 0;
transform: rotate(315deg);
}
.react-resizable-handle-n,
.react-resizable-handle-s {
left: 50%;
margin-left: -10px;
cursor: ns-resize;
}
.react-resizable-handle-n {
top: 0;
transform: rotate(225deg);
}
.react-resizable-handle-s {
bottom: 0;
transform: rotate(45deg);
}
.react-grid-layout {
position: relative;
transition: height 200ms ease;
}
.react-grid-item {
transition: all 200ms ease;
transition-property: left, top;
}
.react-grid-item.cssTransforms {
transition-property: transform;
}
.react-grid-item.resizing {
z-index: 1;
will-change: width, height;
}
.react-grid-item.react-draggable-dragging {
transition: none;
z-index: 3;
will-change: transform;
}
.react-grid-item.dropping {
visibility: hidden;
}
.react-grid-item.react-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.react-grid-item > .react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-grid-item > .react-resizable-handle::after {
content: "";
position: absolute;
right: 3px;
bottom: 3px;
width: 5px;
height: 5px;
border-right: 2px solid rgba(0, 0, 0, 0.4);
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}
.react-resizable-hide > .react-resizable-handle {
display: none;
}

View File

@@ -1,154 +1,12 @@
.react-resizable {
position: relative;
}
.react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
background-repeat: no-repeat;
background-origin: content-box;
box-sizing: border-box;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2IDYiIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSI2cHgiIGhlaWdodD0iNnB4Ij48ZyBvcGFjaXR5PSIwLjMwMiI+PHBhdGggZD0iTSA2IDYgTCAwIDYgTCAwIDQuMiBMIDQgNC4yIEwgNC4yIDQuMiBMIDQuMiAwIEwgNiAwIEwgNiA2IEwgNiA2IFoiIGZpbGw9IiMwMDAwMDAiLz48L2c+PC9zdmc+");
background-position: bottom right;
padding: 0 3px 3px 0;
}
.react-resizable-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
transform: rotate(90deg);
}
.react-resizable-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-resizable-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
transform: rotate(180deg);
}
.react-resizable-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
transform: rotate(270deg);
}
.react-resizable-handle-w,
.react-resizable-handle-e {
top: 50%;
margin-top: -10px;
cursor: ew-resize;
}
.react-resizable-handle-w {
left: 0;
transform: rotate(135deg);
}
.react-resizable-handle-e {
right: 0;
transform: rotate(315deg);
}
.react-resizable-handle-n,
.react-resizable-handle-s {
left: 50%;
margin-left: -10px;
cursor: ns-resize;
}
.react-resizable-handle-n {
top: 0;
transform: rotate(225deg);
}
.react-resizable-handle-s {
bottom: 0;
transform: rotate(45deg);
}
.react-grid-layout {
position: relative;
transition: height 200ms ease;
}
.react-grid-item {
transition: all 200ms ease;
transition-property: left, top;
}
.react-grid-item.cssTransforms {
transition-property: transform;
}
.react-grid-item.resizing {
z-index: 1;
will-change: width, height;
}
.react-grid-item.react-draggable-dragging {
transition: none;
z-index: 3;
will-change: transform;
}
.react-grid-item.dropping {
visibility: hidden;
}
.react-grid-item.react-grid-placeholder {
background: red;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.react-grid-item > .react-resizable-handle {
position: absolute;
width: 20px;
height: 20px;
bottom: 0;
right: 0;
cursor: se-resize;
}
.react-grid-item > .react-resizable-handle::after {
content: "";
position: absolute;
right: 3px;
bottom: 3px;
width: 5px;
height: 5px;
border-right: 2px solid rgba(0, 0, 0, 0.4);
border-bottom: 2px solid rgba(0, 0, 0, 0.4);
}
.react-resizable-hide > .react-resizable-handle {
display: none;
}
.dashboard-card {
height: 100%;
width: 100%;
// background-color: green;
.ant-card-body {
height: 80%;
width: 100%;
// // background-color: red;
// height: 90%;
// width: 100%;
// padding: 8px;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
}
.ant-spin-nested-loading {
// background-color: red;
height: 100%;
.ant-spin-container {
height: 100%;
.ant-table {
height: 100%;
.ant-table-container {
height: 100%;
}
}
}
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
}

View File

@@ -1,3 +0,0 @@
export function GenerateDashboardData(data) {
return data;
}

View File

@@ -1,135 +0,0 @@
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>
);
}

View File

@@ -1,105 +0,0 @@
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>
);
}

View File

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

View File

@@ -1,121 +0,0 @@
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>
);
}

View File

@@ -1,55 +0,0 @@
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;
}
}

View File

@@ -1,340 +0,0 @@
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>
);
}

View File

@@ -1,113 +0,0 @@
//import "tui-image-editor/dist/tui-image-editor.css";
import { Result } from "antd";
import * as markerjs2 from "markerjs2";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function DocumentEditorComponent({ currentUser, bodyshop, document }) {
const imgRef = useRef(null);
const [loading, setLoading] = useState(false);
const [uploaded, setuploaded] = useState(false);
const markerArea = useRef(null);
const { t } = useTranslation();
const triggerUpload = useCallback(
async (dataUrl) => {
setLoading(true);
handleUpload(
{
filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`,
file: await b64toBlob(dataUrl),
onSuccess: () => {
setLoading(false);
setuploaded(true);
},
onError: () => setLoading(false),
},
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: document.jobid,
//billId: billId,
tagsArray: ["edited"],
//callback: callbackAfterUpload,
}
);
},
[bodyshop, currentUser, document]
);
useEffect(() => {
if (imgRef.current !== null) {
// create a marker.js MarkerArea
markerArea.current = new markerjs2.MarkerArea(imgRef.current);
console.log(`markerArea.current`, markerArea.current);
// attach an event handler to assign annotated image back to our image element
markerArea.current.addCloseEventListener((closeEvent) => {
console.log("Close Event", closeEvent);
});
markerArea.current.addRenderEventListener((dataUrl) => {
imgRef.current.src = dataUrl;
markerArea.current.close();
triggerUpload(dataUrl);
});
// launch marker.js
markerArea.current.renderAtNaturalSize = true;
markerArea.current.renderImageType = "image/jpeg";
markerArea.current.renderImageQuality = 1;
//markerArea.current.settings.displayMode = "inline";
markerArea.current.show();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [triggerUpload]);
async function b64toBlob(url) {
const res = await fetch(url);
return await res.blob();
}
return (
<div>
{!loading && !uploaded && (
<img
ref={imgRef}
src={GenerateSrcUrl(document)}
alt="sample"
crossOrigin="anonymous"
style={{ maxWidth: "90vw", maxHeight: "90vh" }}
/>
)}
{loading && <LoadingSpinner message={t("documents.labels.uploading")} />}
{uploaded && (
<Result
status="success"
title={t("documents.successes.edituploaded")}
/>
)}
</div>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(DocumentEditorComponent);

View File

@@ -1,59 +0,0 @@
import { useQuery } from "@apollo/client";
import { Result } from "antd";
import queryString from "query-string";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries";
import { setBodyshop } from "../../redux/user/user.actions";
import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import DocumentEditor from "./document-editor.component";
const mapDispatchToProps = (dispatch) => ({
setBodyshop: (bs) => dispatch(setBodyshop(bs)),
});
export default connect(null, mapDispatchToProps)(DocumentEditorContainer);
export function DocumentEditorContainer({ setBodyshop }) {
//Get the image details for the image to be saved.
//Get the document id from the search string.
const { documentId } = queryString.parse(useLocation().search);
const { t } = useTranslation();
const {
loading: loadingShop,
error: errorShop,
data: dataShop,
} = useQuery(QUERY_BODYSHOP, {
fetchPolicy: "network-only",
});
useEffect(() => {
if (dataShop) setBodyshop(dataShop.bodyshops[0]);
}, [dataShop, setBodyshop]);
const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, {
variables: { documentId },
skip: !documentId,
});
if (loading || loadingShop) return <LoadingSpinner />;
if (error || errorShop)
return (
<AlertComponent
message={error.message || errorShop.message}
type="error"
/>
);
if (!data || !data.documents_by_pk)
return <Result status="404" title={t("general.errors.notfound")} />;
return (
<div>
<DocumentEditor document={data ? data.documents_by_pk : null} />
</div>
);
}

View File

@@ -5,8 +5,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
import client from "../../utils/GraphQLClient";
import exifr from "exifr";
//Context: currentUserEmail, bodyshop, jobid, invoiceid
//Required to prevent headers from getting set and rejected from Cloudinary.
@@ -21,13 +19,8 @@ export const handleUpload = (ev, context) => {
const { onError, onSuccess, onProgress } = ev;
const { bodyshop, jobId } = context;
const fileName = ev.file.name || ev.filename;
let key = `${bodyshop.id}/${jobId}/${fileName.replace(
/\.[^/.]+$/,
""
)}-${new Date().getTime()}`;
let extension = fileName.split(".").pop();
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
let extension = ev.file.name.split(".").pop();
uploadToCloudinary(
key,
extension,
@@ -92,7 +85,6 @@ export const uploadToCloudinary = async (
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
},
};
const formData = new FormData();
formData.append("file", file);
@@ -130,16 +122,6 @@ export const uploadToCloudinary = async (
}
//Insert the document with the matching key.
let takenat;
if (fileType.includes("image")) {
try {
const exif = await exifr.parse(file);
takenat = exif && exif.DateTimeOriginal;
} catch (error) {
console.log("Unable to parse image file for EXIF Data");
}
}
const documentInsert = await client.mutate({
mutation: INSERT_NEW_DOCUMENT,
variables: {
@@ -150,10 +132,9 @@ export const uploadToCloudinary = async (
uploaded_by: uploaded_by,
key: key,
type: fileType,
extension: cloudinaryUploadResponse.data.format || extension,
extension: extension,
bodyshopid: bodyshop.id,
size: cloudinaryUploadResponse.data.bytes || file.size,
takenat,
},
],
},
@@ -185,7 +166,6 @@ export const uploadToCloudinary = async (
}
};
//Also needs to be updated in media JS and mobile app.
export function DetermineFileType(filetype) {
if (!filetype) return "auto";
else if (filetype.startsWith("image")) return "image";

View File

@@ -1,58 +0,0 @@
import { useQuery } from "@apollo/client";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectEmailConfig } from "../../redux/email/email.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
emailConfig: selectEmailConfig,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(EmailDocumentsComponent);
export function EmailDocumentsComponent({
emailConfig,
selectedMediaState,
}) {
const { t } = useTranslation();
const [selectedMedia, setSelectedMedia] = selectedMediaState;
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: {
jobId: emailConfig.jobid,
},
skip: !emailConfig.jobid,
});
console.log(
"🚀 ~ file: email-documents.component.jsx ~ line 38 ~ emailConfig",
emailConfig
);
return (
<div>
{loading && <LoadingSpinner />}
{error && <AlertComponent message={error.message} type="error" />}
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null}
{data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
</div>
);
}

View File

@@ -1,10 +1,9 @@
import { UploadOutlined } from "@ant-design/icons";
import { Divider, Form, Input, Select, Tabs, Upload } from "antd";
import { Card, Divider, Form, Input, Select, Upload } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import EmailDocumentsComponent from "../email-documents/email-documents.component";
export default function EmailOverlayComponent({ form, selectedMediaState }) {
export default function EmailOverlayComponent({ form }) {
const { t } = useTranslation();
return (
<div>
@@ -37,8 +36,6 @@ export default function EmailOverlayComponent({ form, selectedMediaState }) {
</Form.Item>
<Divider>{t("emails.labels.preview")}</Divider>
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
<Form.Item shouldUpdate>
{() => {
return (
@@ -55,38 +52,34 @@ export default function EmailOverlayComponent({ form, selectedMediaState }) {
}}
</Form.Item>
<Tabs>
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
</Tabs.TabPane>
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
<Form.Item
name="fileList"
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
<Card title={t("emails.labels.attachments")}>
<Form.Item
name="fileList"
valuePropName="fileList"
getValueFromEvent={(e) => {
console.log("Upload event:", e);
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}
multiple
listType="picture-card"
>
<Upload.Dragger
beforeUpload={Upload.LIST_IGNORE}
multiple
listType="picture-card"
>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</Tabs.TabPane>
</Tabs>
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</Card>
</div>
);
}

View File

@@ -43,12 +43,6 @@ export function EmailOverlayContainer({
const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false);
const [rawHtml, setRawHtml] = useState("");
const [pdfCopytoAttach, setPdfCopytoAttach] = useState({
filename: null,
pdf: null,
});
const [selectedMedia, setSelectedMedia] = useState([]);
const defaultEmailFrom = {
from: {
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
@@ -62,18 +56,17 @@ export function EmailOverlayContainer({
const handleFinish = async (values) => {
logImEXEvent("email_send_from_modal");
console.log(`values`, values);
const attachments = [];
//const attachments = [];
// if (values.fileList)
// await asyncForEach(values.fileList, async (f) => {
// const t = {
// ContentType: f.type,
// Filename: f.name,
// Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
// };
// attachments.push(t);
// });
await asyncForEach(values.fileList, async (f) => {
const t = {
ContentType: f.type,
Filename: f.name,
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
};
attachments.push(t);
});
setSending(true);
try {
@@ -81,29 +74,9 @@ export function EmailOverlayContainer({
...defaultEmailFrom,
...values,
html: rawHtml,
attachments: [
...(values.fileList
? await Promise.all(
values.fileList.map(async (f) => {
return {
filename: f.name,
path: await toBase64(f.originFileObj),
};
})
)
: []),
...(pdfCopytoAttach.pdf
? [
{
path: pdfCopytoAttach.pdf,
filename:
pdfCopytoAttach.filename &&
`${pdfCopytoAttach.filename}.pdf`,
},
]
: []),
],
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
attachments: await Promise.all(
values.fileList.map(async (f) => await toBase64(f.originFileObj))
),
//attachments,
});
notification["success"]({ message: t("emails.successes.sent") });
@@ -120,22 +93,13 @@ export function EmailOverlayContainer({
const render = async () => {
logImEXEvent("email_render_template", { template: emailConfig.template });
setLoading(true);
let { html, pdf, filename } = await RenderTemplate(
emailConfig.template,
bodyshop,
true
);
let html = await RenderTemplate(emailConfig.template, bodyshop, true);
const response = await axios.post("/render/inlinecss", {
html: html,
url: `${window.location.protocol}://${window.location.host}/`,
});
setRawHtml(response.data);
if (pdf) {
setPdfCopytoAttach({ pdf, filename });
}
form.setFieldsValue({
...emailConfig.messageOptions,
cc:
@@ -173,12 +137,7 @@ export function EmailOverlayContainer({
<LoadingSpinner message={t("emails.labels.generatingemail")} />
</div>
)}
{!loading && (
<EmailOverlayComponent
form={form}
selectedMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
{!loading && <EmailOverlayComponent form={form} />}
</Form>
</Modal>
);
@@ -196,8 +155,8 @@ const toBase64 = (file) =>
reader.onerror = (error) => reject(error);
});
// const asyncForEach = async (array, callback) => {
// for (let index = 0; index < array.length; index++) {
// await callback(array[index], index, array);
// }
// };
const asyncForEach = async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
};

View File

@@ -5,15 +5,9 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { tracker } from "../../App/App.container";
import { selectCurrentUser } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -37,41 +31,24 @@ 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 = () => {
window.$crisp.push([
"do",
"message:send",
[
"text",
`I hit the following error: \n\n
${this.state.error.message}\n\n
${this.state.error.stack}\n\n
URL:${window.location} as ${this.props.currentUser.email} for ${
this.props.bodyshop && this.props.bodyshop.name
}
`,
],
]);
const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
window.$crisp.push(["do", "chat:open"]);
// const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
----
System Generated Log:
${this.state.error.message}
${this.state.error.stack}
`;
// ----
// System Generated Log:
// ${this.state.error.message}
// ${this.state.error.stack}
// `;
// const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
// errorDescription
// )}&customfield_10049=${window.location}&email=${
// this.props.currentUser.email
// }`;
// console.log(`URL`, URL);
// window.open(URL, "_blank");
const URL = `https://bodyshop.atlassian.net/servicedesk/customer/portal/3/group/8/create/26?summary=123&description=${encodeURI(
errorDescription
)}&customfield_10049=${window.location}&email=${
this.props.currentUser.email
}`;
console.log(`URL`, URL);
window.open(URL, "_blank");
};
render() {
@@ -80,23 +57,6 @@ class ErrorBoundary extends React.Component {
if (this.state.hasErrored === true) {
logImEXEvent("error_boundary_rendered", { error, info });
window.$crisp.push([
"set",
"session:event",
[
[
[
"error_boundary",
{
error: this.state.error.message,
stack: this.state.error.stack,
},
"red",
],
],
],
]);
return (
<div>
<Result
@@ -114,7 +74,7 @@ class ErrorBoundary extends React.Component {
{t("general.actions.refresh")}
</Button>
<Button onClick={this.handleErrorSubmit}>
{t("general.actions.senderrortosupport")}
{t("general.actions.submitticket")}
</Button>
</Space>
}

View File

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

View File

@@ -1,50 +0,0 @@
import moment from "moment";
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 AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
function FeatureWrapper({
bodyshop,
featureName,
noauth,
children,
...restProps
}) {
const { t } = useTranslation();
if (HasFeatureAccess({ featureName, bodyshop })) return children;
return (
noauth || (
<AlertComponent
message={t("general.messages.nofeatureaccess")}
type="warning"
/>
)
);
}
export function HasFeatureAccess({ featureName, bodyshop }) {
return (
bodyshop.features.allAccess ||
moment(bodyshop.features[featureName]).isAfter(moment())
);
}
export default connect(mapStateToProps, null)(FeatureWrapper);
/*
dashboard
production-board
scoreboard
csi
tech-console
mobile-imaging
*/

View File

@@ -11,8 +11,9 @@ import AlertComponent from "../alert/alert.component";
export default function GlobalSearch() {
const { t } = useTranslation();
const [callSearch, { loading, error, data }] =
useLazyQuery(GLOBAL_SEARCH_QUERY);
const [callSearch, { loading, error, data }] = useLazyQuery(
GLOBAL_SEARCH_QUERY
);
const executeSearch = (v) => {
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
@@ -37,7 +38,7 @@ export default function GlobalSearch() {
value: job.ro_number,
label: (
<Link to={`/manage/jobs/${job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<Space wrap split={<Divider type="vertical" />}>
<strong>{job.ro_number || t("general.labels.na")}</strong>
<span>{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
job.ownr_co_nm || ""
@@ -45,7 +46,7 @@ export default function GlobalSearch() {
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
job.v_model_desc || ""
}`}</span>
<span>{`${job.clm_no || ""}`}</span>
<span>{`${job.clm_no}`}</span>
</Space>
</Link>
),
@@ -62,16 +63,13 @@ export default function GlobalSearch() {
}`,
label: (
<Link to={`/manage/owners/${owner.id}`}>
<Space size="small" split={<Divider type="vertical" />} wrap>
<Space wrap split={<Divider type="vertical" />}>
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
owner.ownr_co_nm || ""
}`}</span>
<PhoneNumberFormatter>
{owner.ownr_ph1}
</PhoneNumberFormatter>
<PhoneNumberFormatter>
{owner.ownr_ph2}
</PhoneNumberFormatter>
</Space>
</Link>
),
@@ -88,14 +86,14 @@ export default function GlobalSearch() {
} ${vehicle.v_model_desc || ""}`,
label: (
<Link to={`/manage/vehicles/${vehicle.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<Space wrap split={<Divider type="vertical" />}>
<span>
{`${vehicle.v_model_yr || ""} ${
vehicle.v_make_desc || ""
} ${vehicle.v_model_desc || ""}`}
</span>
<span>{vehicle.plate_no || ""}</span>
<span> {vehicle.v_vin || ""}</span>
<span>{vehicle.plate_no}</span>
<span> {vehicle.v_vin}</span>
</Space>
</Link>
),
@@ -110,12 +108,11 @@ export default function GlobalSearch() {
value: `${payment.job.ro_number} ${payment.payer} ${payment.amount}`,
label: (
<Link to={`/manage/jobs/${payment.job.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<span>{payment.paymentnum}</span>
<Space wrap split={<Divider type="vertical" />}>
<span>{payment.job.ro_number}</span>
<span>{payment.memo || ""}</span>
<span>{payment.amount || ""}</span>
<span>{payment.transactionid || ""}</span>
<span>{payment.job.memo}</span>
<span>{payment.job.amount}</span>
<span>{payment.job.transactionid}</span>
</Space>
</Link>
),
@@ -130,7 +127,7 @@ export default function GlobalSearch() {
value: `${bill.invoice_number} - ${bill.vendor.name}`,
label: (
<Link to={`/manage/bills?billid=${bill.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<Space wrap split={<Divider type="vertical" />}>
<span>{bill.invoice_number}</span>
<span>{bill.vendor.name}</span>
<span>{bill.date}</span>
@@ -150,7 +147,7 @@ export default function GlobalSearch() {
}`,
label: (
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
<Space size="small" split={<Divider type="vertical" />}>
<Space wrap split={<Divider type="vertical" />}>
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
pb.company || ""
}`}</span>
@@ -169,9 +166,10 @@ export default function GlobalSearch() {
return (
<AutoComplete
dropdownMatchSelectWidth={"false"}
options={options}
onSearch={handleSearch}
placeholder={t("general.labels.globalsearch")}
allowClear
>
<Input.Search loading={loading} />
</AutoComplete>

View File

@@ -3,12 +3,10 @@ import Icon, {
BarChartOutlined,
CarFilled,
ClockCircleFilled,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
FieldTimeOutlined,
FileAddFilled,
FileAddOutlined,
FileFilled,
GlobalOutlined,
HomeFilled,
@@ -16,7 +14,6 @@ import Icon, {
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
QuestionCircleFilled,
ScheduleOutlined,
SettingOutlined,
TeamOutlined,
@@ -47,6 +44,7 @@ import {
import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -80,11 +78,12 @@ function Header({
const { t } = useTranslation();
return (
<Layout.Header>
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
<Menu
mode="horizontal"
//theme="light"
theme={"dark"}
style={{ flex: 1 }}
selectedKeys={[selectedHeader]}
onClick={handleMenuClick}
subMenuCloseDelay={0.3}
@@ -96,7 +95,6 @@ function Header({
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
</Menu.Item>
<Menu.SubMenu
key="jobssubmenu"
icon={<Icon component={FaCarCrash} />}
title={t("menus.header.jobs")}
>
@@ -111,14 +109,12 @@ function Header({
{t("menus.header.availablejobs")}
</Link>
</Menu.Item>
<Menu.Item key="newjob" icon={<FileAddOutlined />}>
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
</Menu.Item>
<Menu.Divider key="div1" />
<Menu.Divider />
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
</Menu.Item>
<Menu.Divider key="div2" />
<Menu.Divider />
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
<Link to="/manage/production/list">
{t("menus.header.productionlist")}
@@ -129,13 +125,13 @@ function Header({
{t("menus.header.productionboard")}
</Link>
</Menu.Item>
<Menu.Divider key="div3" />
<Menu.Divider />
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
key="customers"
icon={<UserOutlined />}
title={t("menus.header.customers")}
>
@@ -147,7 +143,6 @@ function Header({
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
key="ccs"
icon={<CarFilled />}
title={t("menus.header.courtesycars")}
>
@@ -168,7 +163,6 @@ function Header({
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
key="accounting"
icon={<DollarCircleFilled />}
title={t("menus.header.accounting")}
>
@@ -190,7 +184,7 @@ function Header({
>
{t("menus.header.enterbills")}
</Menu.Item>
<Menu.Divider key="div4" />
<Menu.Divider />
<Menu.Item key="allpayments" icon={<BankFilled />}>
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
</Menu.Item>
@@ -202,11 +196,11 @@ function Header({
context: null,
});
}}
icon={<Icon component={FaCreditCard} />}
>
<Icon component={FaCreditCard} />
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Divider key="div5" />
<Menu.Divider />
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
<Link to="/manage/timetickets">
@@ -225,10 +219,9 @@ function Header({
>
{t("menus.header.entertimeticket")}
</Menu.Item>
<Menu.Divider key="div6" />
<Menu.Divider />
<Menu.SubMenu
key="accountingexport"
title={t("menus.header.export")}
icon={<ExportOutlined />}
>
@@ -262,17 +255,11 @@ function Header({
{t("menus.header.temporarydocs")}
</Link>
</Menu.Item>
<Menu.SubMenu
key="shopsubmenu"
title={t("menus.header.shop")}
icon={<SettingOutlined />}
>
<Menu.SubMenu title={t("menus.header.shop")} icon={<SettingOutlined />}>
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
</Menu.Item>
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
</Menu.Item>
<Menu.Item
key="reportcenter"
icon={<BarChartOutlined />}
@@ -298,27 +285,17 @@ function Header({
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
key="user"
style={{ float: "right" }}
title={
currentUser.displayName ||
currentUser.email ||
t("general.labels.unknown")
}
>
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
<Menu.Item danger onClick={() => signOutStart()}>
{t("user.actions.signout")}
</Menu.Item>
<Menu.Item
key="help"
onClick={() => {
window.open("https://help.imex.online/", "_blank");
}}
icon={<Icon component={QuestionCircleFilled} />}
>
{t("menus.header.help")}
</Menu.Item>
<Menu.Item
key="rescue"
onClick={() => {
window.open("https://imexrescue.com/", "_blank");
}}
@@ -332,7 +309,6 @@ function Header({
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
</Menu.Item>
<Menu.SubMenu
key="langselecter"
title={
<span>
<GlobalOutlined />
@@ -351,7 +327,7 @@ function Header({
</Menu.Item>
</Menu.SubMenu>
</Menu.SubMenu>
<Menu.SubMenu key="recent" title={<ClockCircleFilled />}>
<Menu.SubMenu style={{ float: "right" }} title={<ClockCircleFilled />}>
{recentItems.map((i, idx) => (
<Menu.Item key={idx}>
<Link to={i.url}>{i.label}</Link>
@@ -359,6 +335,9 @@ function Header({
))}
</Menu.SubMenu>
</Menu>
<div>
<GlobalSearch />
</div>
</Layout.Header>
);
}

View File

@@ -0,0 +1,28 @@
import React, { useEffect } from "react";
export default function JiraSupportComponent() {
useScript();
return <div></div>;
}
const useScript = () => {
useEffect(() => {
const script = document.createElement("script");
script.src = "https://jsd-widget.atlassian.com/assets/embed.js";
script.setAttribute("data-jsd-embedded", true);
script.setAttribute("data-key", "d69bb65c-1dd3-483f-b109-66a970d03f44");
script.setAttribute("data-base-url", "https://jsd-widget.atlassian.com");
//script.async = true;
script.onload = () => {
var DOMContentLoaded_event = document.createEvent("Event");
DOMContentLoaded_event.initEvent("DOMContentLoaded", true, true);
window.document.dispatchEvent(DOMContentLoaded_event);
};
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, []);
};

View File

@@ -87,7 +87,9 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
return (
<>
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
<Button type="primary" onClick={showModal}>
{t("printcenter.jobs.3rdpartypayer")}
</Button>
<Modal visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<Form
onFinish={handleFinish}
@@ -161,7 +163,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
<Input />
</Form.Item>
<Form.Item
label={t("printcenter.jobs.3rdpartyfields.refnumber")}
label={t("printcenter.jobs.3rdpartyfields.ponumber")}
name="ponumber"
>
<Input />

View File

@@ -1,50 +1,22 @@
import { AlertFilled } from "@ant-design/icons";
import {
Button,
Divider,
Dropdown,
Menu,
notification,
Popover,
Space,
} from "antd";
import parsePhoneNumber from "libphonenumber-js";
import moment from "moment";
import queryString from "query-string";
import { Button, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
openChatByPhone,
setMessage,
} from "../../redux/messaging/messaging.actions";
import { Link } from "react-router-dom";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import DataLabel from "../data-label/data-label.component";
import ScheduleAtChange from "./job-at-change.component";
import ScheduleEventColor from "./schedule-event.color.component";
import ScheduleEventNote from "./schedule-event.note.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setScheduleContext: (context) =>
dispatch(setModalContext({ context: context, modal: "schedule" })),
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
setMessage: (text) => dispatch(setMessage(text)),
});
export function ScheduleEventComponent({
bodyshop,
setMessage,
openChatByPhone,
event,
refetch,
handleCancel,
@@ -52,8 +24,6 @@ export function ScheduleEventComponent({
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const history = useHistory();
const searchParams = queryString.parse(useLocation().search);
const blockContent = (
<div>
@@ -64,7 +34,7 @@ export function ScheduleEventComponent({
);
const popoverContent = (
<div style={{ maxWidth: "40vw" }}>
<div>
{!event.isintake ? (
<strong>{event.title}</strong>
) : (
@@ -101,101 +71,39 @@ export function ScheduleEventComponent({
{(event.job && event.job.ownr_ea) || ""}
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph1")}>
<ChatOpenButton
phone={event.job && event.job.ownr_ph1}
jobid={event.job.id}
/>
</DataLabel>
<DataLabel label={t("jobs.fields.ownr_ph2")}>
<ChatOpenButton
phone={event.job && event.job.ownr_ph2}
jobid={event.job.id}
/>
<PhoneFormatter>
{(event.job && event.job.ownr_ph1) || ""}
</PhoneFormatter>
</DataLabel>
<DataLabel label={t("jobs.fields.alt_transport")}>
{(event.job && event.job.alt_transport) || ""}
<ScheduleAtChange job={event && event.job} />
</DataLabel>
<ScheduleEventNote event={event} />
</div>
) : null}
<Divider />
<Space wrap>
{event.job ? (
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
<Button>{t("appointments.actions.viewjob")}</Button>
</Link>
) : null}
{event.job ? (
<Button
onClick={() => {
history.push({
search: queryString.stringify({
...searchParams,
selected: event.job.id,
}),
});
}}
>
{t("appointments.actions.preview")}
</Button>
) : null}
<Dropdown
overlay={
<Menu>
<Menu.Item
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{
to: event.job && event.job.ownr_ea,
subject: Template.subject,
},
"e",
event.job && event.job.id
);
}}
disabled={event.arrived}
>
{t("general.labels.email")}
</Menu.Item>
<Menu.Item
onClick={() => {
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
if (p && p.isValid()) {
openChatByPhone({
phone_num: p.formatInternational(),
jobid: event.job.id,
});
setMessage(
t("appointments.labels.reminder", {
shopname: bodyshop.shopname,
date: moment(event.start).format("MM/DD/YYYY"),
time: moment(event.start).format("HH:MM a"),
})
);
setVisible(false);
} else {
notification["error"]({
message: t("messaging.error.invalidphone"),
});
}
}}
disabled={event.arrived || !bodyshop.messagingservicesid}
>
{t("general.labels.sms")}
</Menu.Item>
</Menu>
}
<Button
onClick={() => {
const Template = TemplateList("job").appointment_reminder;
GenerateDocument(
{
name: Template.key,
variables: { id: event.job.id },
},
{ to: event.job && event.job.ownr_ea, subject: Template.subject },
"e"
);
}}
disabled={event.arrived}
>
<Button>{t("appointments.actions.sendreminder")}</Button>
</Dropdown>
{t("appointments.actions.sendreminder")}
</Button>
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
{t("appointments.actions.cancel")}
</Button>
@@ -234,7 +142,6 @@ export function ScheduleEventComponent({
const RegularEvent = event.isintake ? (
<div style={{ display: "flex", flexWrap: "wrap" }}>
<Space>
{event.note && <AlertFilled className="production-alert" />}
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
<span>{`${(event.job && event.job.ownr_fn) || ""} ${
(event.job && event.job.ownr_ln) || ""
@@ -276,7 +183,4 @@ export function ScheduleEventComponent({
</Popover>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScheduleEventComponent);
export default connect(null, mapDispatchToProps)(ScheduleEventComponent);

View File

@@ -1,74 +0,0 @@
import { EditFilled, SaveFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Input, notification, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DataLabel from "../data-label/data-label.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ScheduleEventNote({ event }) {
const [editing, setEditing] = useState(false);
const [note, setNote] = useState(event.note || "");
const [loading, setLoading] = useState(false);
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
const { t } = useTranslation();
const toggleEdit = async () => {
if (editing) {
//Await the update
setLoading(true);
const result = await updateAppointment({
variables: {
appid: event.id,
app: { note },
},
});
if (!!!result.errors) {
// notification["success"]({ message: t("appointments.successes.saved") });
} else {
notification["error"]({
message: t("jobs.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
}
setEditing(false);
} else {
setEditing(true);
}
setLoading(false);
};
return (
<DataLabel label={t("appointments.fields.note")}>
<Space flex>
{!editing ? (
event.note || ""
) : (
<Input.TextArea
rows={3}
value={note}
onChange={(e) => setNote(e.target.value)}
style={{ maxWidth: "8vw" }}
/>
)}
<Button onClick={toggleEdit} loading={loading}>
{editing ? <SaveFilled /> : <EditFilled />}
</Button>
</Space>
</DataLabel>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote);

View File

@@ -1,46 +0,0 @@
import { useQuery } from "@apollo/client";
import { Card, Table } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export default function JobAuditTrail({ jobId }) {
const { t } = useTranslation();
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
variables: { jobid: jobId },
skip: !jobId,
});
const columns = [
{
title: t("audit.fields.created"),
dataIndex: "created",
key: "created",
render: (text, record) => (
<DateTimeFormatter>{record.created}</DateTimeFormatter>
),
},
{
title: t("audit.fields.useremail"),
dataIndex: "useremail",
key: "useremail",
},
{
title: t("audit.fields.operation"),
dataIndex: "operation",
key: "operation",
},
];
return (
<Card title={t("jobs.labels.audit")}>
<Table
loading={loading}
columns={columns}
rowKey="id"
dataSource={data ? data.audit_trail : []}
/>
</Card>
);
}

View File

@@ -15,22 +15,16 @@ import {
} from "../../../../redux/user/user.selectors";
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
import moment from "moment-business-days";
import { insertAuditTrail } from "../../../../redux/application/application.actions";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation }) =>
dispatch(insertAuditTrail({ jobid, operation })),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function JobChecklistForm({
insertAuditTrail,
formItems,
bodyshop,
currentUser,
@@ -42,8 +36,6 @@ export function JobChecklistForm({
const [intakeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
const [updateOwner] = useMutation(UPDATE_OWNER);
const { jobId } = useParams();
const history = useHistory();
const search = queryString.parse(useLocation().search);
@@ -64,16 +56,8 @@ export function JobChecklistForm({
...(type === "intake" && { actual_in: new Date() }),
...(type === "intake" && {
production_vars: {
...(job ? job.production_vars : {}),
note:
values.production_vars &&
values.production_vars.note &&
values.production_vars.note !== ""
? values &&
values.production_vars &&
values.production_vars.note
: job && job.production_vars && job.production_vars.note,
...job.production_vars,
...values.production_vars,
},
}),
...(type === "intake" && {
@@ -97,7 +81,6 @@ export function JobChecklistForm({
...(type === "deliver" && {
scheduled_delivery: values.scheduled_delivery,
actual_delivery: values.actual_delivery,
}),
...(type === "deliver" &&
values.removeFromProduction && {
@@ -119,40 +102,11 @@ export function JobChecklistForm({
});
}
}
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);
if (!!!result.errors) {
notification["success"]({ message: t("checklist.successes.completed") });
history.push(`/manage/jobs/${jobId}`);
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobchecklist(
type,
(type === "deliver" && values.removeFromProduction && false) ||
(type === "intake" && values.addToProduction),
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
),
});
} else {
notification["error"]({
message: t("checklist.errors.complete", {
@@ -167,11 +121,7 @@ export function JobChecklistForm({
title={t("checklist.labels.checklist")}
extra={
!readOnly && (
<Button
loading={loading}
type="primary"
onClick={() => form.submit()}
>
<Button loading={loading} onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
)
@@ -183,21 +133,12 @@ export function JobChecklistForm({
initialValues={{
...(type === "intake" && {
addToProduction: true,
allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion:
(job && job.scheduled_completion) ||
moment().businessAdd(
(job.labhrs.aggregate.sum.mod_lb_hrs +
job.larhrs.aggregate.sum.mod_lb_hrs) /
bodyshop.target_touchtime,
"days"
),
scheduled_completion: job && job.scheduled_completion,
scheduled_delivery: job && job.scheduled_delivery,
}),
...(type === "deliver" && {
removeFromProduction: true,
actual_completion: job && job.actual_completion,
actual_delivery: job && job.actual_delivery,
}),
...formItems
.filter((fi) => fi.value)
@@ -219,14 +160,6 @@ export function JobChecklistForm({
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item
name="allow_text_message"
valuePropName="checked"
label={t("checklist.labels.allow_text_message")}
disabled={readOnly}
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item
name="scheduled_completion"
label={t("jobs.fields.scheduled_completion")}
@@ -238,22 +171,21 @@ export function JobChecklistForm({
},
]}
>
<DateTimePicker disabled={readOnly} />
<DateTimePicker />
</Form.Item>
<Form.Item
name="scheduled_delivery"
label={t("jobs.fields.scheduled_delivery")}
disabled={readOnly}
>
<DateTimePicker disabled={readOnly} />
<DateTimePicker />
</Form.Item>
<Form.Item
name={["production_vars", "note"]}
label={t("jobs.fields.production_vars.note")}
disabled={readOnly}
trigger="onChange"
>
<Input.TextArea rows={3} disabled={readOnly} />
<Input.TextArea rows={3} />
</Form.Item>
</div>
)}
@@ -270,14 +202,7 @@ export function JobChecklistForm({
},
]}
>
<DateTimePicker disabled={readOnly} />
</Form.Item>
<Form.Item
name="actual_delivery"
label={t("jobs.fields.actual_delivery")}
disabled={readOnly}
>
<DateTimePicker disabled={readOnly} />
<DateTimePicker />
</Form.Item>
<Form.Item
name="removeFromProduction"

View File

@@ -2,6 +2,7 @@ import React from "react";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
export default function JobChecklistDisplay({ checklist }) {
console.log("JobChecklistDisplay -> checklist", checklist);
if (!checklist) return <div></div>;
return (
<div>

View File

@@ -112,46 +112,60 @@ export function JobDetailCards({ setPrintCenterContext }) {
<Divider type="horizontal" />
<Row gutter={[16, 16]}>
<Col {...span}>
<JobDetailCardsInsuranceComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsInsuranceComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<JobDetailCardsTotalsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsTotalsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<JobDetailCardsDatesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsDatesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<JobDetailCardsPartsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsPartsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<JobDetailCardsNotesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsNotesComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
<Col {...span}>
<JobDetailCardsDamageComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<Card.Grid style={{ width: "100%", height: "100%" }}>
<JobDetailCardsDamageComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Card.Grid>
</Col>
</Row>
</Card>

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