Compare commits
187 Commits
developmen
...
feature/cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d6c5e1b1 | ||
|
|
84b39f3d2b | ||
|
|
4ab0947cc8 | ||
|
|
105ecd4221 | ||
|
|
11af41f3c0 | ||
|
|
6a24c10225 | ||
|
|
8c50589eba | ||
|
|
eaa134d474 | ||
|
|
3d61d95e44 | ||
|
|
d9d3c899a1 | ||
|
|
00f71eba77 | ||
|
|
1e547f1815 | ||
|
|
4b7bbe686a | ||
|
|
f744acd131 | ||
|
|
2be61379f8 | ||
|
|
227a1034cd | ||
|
|
a2f3d9a1b6 | ||
|
|
33f863e1e6 | ||
|
|
630dacd8bf | ||
|
|
4e161248b3 | ||
|
|
2172cc2d04 | ||
|
|
b49555e111 | ||
|
|
d634fcd4cf | ||
|
|
a7e972b3ce | ||
|
|
35273c64bd | ||
|
|
5be2d7bd39 | ||
|
|
749dfc0fba | ||
|
|
4f6bb02ab7 | ||
|
|
5a109c5752 | ||
|
|
756e363e92 | ||
|
|
d90a0cd0c8 | ||
|
|
6de9007c3a | ||
|
|
0b2584e2f1 | ||
|
|
1f59d114e8 | ||
|
|
69ac8617da | ||
|
|
ed00b4550c | ||
|
|
12bc4faf48 | ||
|
|
471c918ac3 | ||
|
|
012f256b77 | ||
|
|
0c23c16f3b | ||
|
|
ae67033417 | ||
|
|
1489d5cd5a | ||
|
|
0adb34b4d3 | ||
|
|
989c7b2ba0 | ||
|
|
1575d5e3e7 | ||
|
|
859522b028 | ||
|
|
ba8c8bc976 | ||
|
|
145e3e5c44 | ||
|
|
914a7e3c7b | ||
|
|
e6cb804055 | ||
|
|
f20ef2d11d | ||
|
|
471df3b659 | ||
|
|
b12ad405c3 | ||
|
|
a218564a24 | ||
|
|
a42da5b6da | ||
|
|
db76992c70 | ||
|
|
4071abcb56 | ||
|
|
3ab31c8bee | ||
|
|
8d74ef275e | ||
|
|
5b9640a1de | ||
|
|
f8c1087360 | ||
|
|
2368aeb5e8 | ||
|
|
f81e026e12 | ||
|
|
bb2d62a11c | ||
|
|
21a1791e7a | ||
|
|
75606559c6 | ||
|
|
0615e46d8a | ||
|
|
a4b58b4bd9 | ||
|
|
41c30fe704 | ||
|
|
7745848961 | ||
|
|
7a8e8de724 | ||
|
|
a45bf6d959 | ||
|
|
c6df38e753 | ||
|
|
0fa214f029 | ||
|
|
ef06e67c9f | ||
|
|
9ddb83a761 | ||
|
|
9b881ee11a | ||
|
|
87d2618020 | ||
|
|
bd2f22f059 | ||
|
|
170f03979e | ||
|
|
66f98656b0 | ||
|
|
e801a03984 | ||
|
|
979ba1c142 | ||
|
|
784c58e295 | ||
|
|
7c6b2faa1a | ||
|
|
fb5a146dee | ||
|
|
a8b555b773 | ||
|
|
dbe3944089 | ||
|
|
aa60424eae | ||
|
|
95ba0e1f8a | ||
|
|
afeb2b94cd | ||
|
|
0b50f424fa | ||
|
|
786d76bb73 | ||
|
|
8427ea208b | ||
|
|
701a52dd22 | ||
|
|
b897795c27 | ||
|
|
9dfff36edf | ||
|
|
9d4f98d3ee | ||
|
|
8770e95ee3 | ||
|
|
9ff9baa78c | ||
|
|
28326f2628 | ||
|
|
7c35a2e790 | ||
|
|
61c57e1866 | ||
|
|
488d1be1cc | ||
|
|
11d3e3c7ad | ||
|
|
a61b4a47cf | ||
|
|
0108135fe9 | ||
|
|
372b7e8e30 | ||
|
|
74b3ceec63 | ||
|
|
144fe06e60 | ||
|
|
bef01385b2 | ||
|
|
fa3ccf0fea | ||
|
|
d2bb7121cb | ||
|
|
2dbcd203ce | ||
|
|
1eb65dbc43 | ||
|
|
b066e08511 | ||
|
|
214d07f2ef | ||
|
|
16746dd2a0 | ||
|
|
bcec0cd902 | ||
|
|
6faf69a875 | ||
|
|
6f229a859b | ||
|
|
092904377f | ||
|
|
7277688de4 | ||
|
|
9e5ff432d7 | ||
|
|
5817daa4b3 | ||
|
|
51292f50dc | ||
|
|
b5e9e75751 | ||
|
|
f778b964fd | ||
|
|
06c14d2742 | ||
|
|
7441288cf5 | ||
|
|
7524cdf0b1 | ||
|
|
46dff9f52c | ||
|
|
afb0c85e9f | ||
|
|
af6bb18db2 | ||
|
|
c28d4c15a0 | ||
|
|
0c167a1833 | ||
|
|
f7938df5e4 | ||
|
|
5717677339 | ||
|
|
cffe9cd4f6 | ||
|
|
5da8c77b3a | ||
|
|
3cc652d113 | ||
|
|
7d82fb8f04 | ||
|
|
f1cbc9f775 | ||
|
|
58c6ba2f87 | ||
|
|
0fbd4b495b | ||
|
|
343274e1e2 | ||
|
|
bc729c0f8c | ||
|
|
09ce5ac892 | ||
|
|
2536b0b986 | ||
|
|
c8506387f6 | ||
|
|
79f2c7dd3d | ||
|
|
d91a83a137 | ||
|
|
0b21b8d976 | ||
|
|
842cb54867 | ||
|
|
a5628188d8 | ||
|
|
1b0e37be45 | ||
|
|
48ecfe0d98 | ||
|
|
b5b4a3a4f9 | ||
|
|
585d585cd3 | ||
|
|
42356c4e99 | ||
|
|
cd9aeba9f7 | ||
|
|
8a7a5f35c1 | ||
|
|
b24c5e7cf1 | ||
|
|
e8b7e2f0b9 | ||
|
|
b75e14e4f5 | ||
|
|
3e59c60477 | ||
|
|
e89b4fe2a4 | ||
|
|
73b0542b62 | ||
|
|
c9812c36c0 | ||
|
|
d62a2c0aaf | ||
|
|
ee15f063ce | ||
|
|
5f0a683ec1 | ||
|
|
6ae9de7df3 | ||
|
|
19aa41ab5b | ||
|
|
0494b3a9a6 | ||
|
|
01d9dc9033 | ||
|
|
47b8d8d8b2 | ||
|
|
6e3453cd90 | ||
|
|
4e10451bea | ||
|
|
81d5c8f449 | ||
|
|
79fa71fc84 | ||
|
|
f6378daa89 | ||
|
|
f2a4eb1b65 | ||
|
|
2d19c35177 | ||
|
|
9787f7e377 | ||
|
|
3abac3e7ac | ||
|
|
fddf75b40b |
6
_reference/Responsibility Center Setup.md
Normal file
@@ -22,7 +22,7 @@ hooks.js:
|
|||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
path: "/pull",
|
path: "/pull",
|
||||||
command: "git pull && npm i",
|
command: "git pull && yarn && pm2 restart 0",
|
||||||
cwd: "/home/ubuntu/io/",
|
cwd: "/home/ubuntu/io/",
|
||||||
method: "post",
|
method: "post",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import JobsShow from "../jobs/jobs.show";
|
|||||||
const httpLink = new HttpLink({
|
const httpLink = new HttpLink({
|
||||||
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
||||||
headers: {
|
headers: {
|
||||||
"x-hasura-admin-secret": `Dev-BodyShopAppBySnaptSoftware!`,
|
"x-hasura-admin-secret": `Dev-BodyShopApp!`,
|
||||||
// 'Authorization': `Bearer xxxx`,
|
// 'Authorization': `Bearer xxxx`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -67,7 +67,7 @@ const client = new ApolloClient({
|
|||||||
// uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
// uri: process.env.REACT_APP_GRAPHQL_ENDPOINT,
|
||||||
// cache: new InMemoryCache(),
|
// cache: new InMemoryCache(),
|
||||||
// headers: {
|
// headers: {
|
||||||
// "x-hasura-admin-secret": `Dev-BodyShopAppBySnaptSoftware!`,
|
// "x-hasura-admin-secret": `Dev-BodyShopApp!`,
|
||||||
// // 'Authorization': `Bearer xxxx`,
|
// // 'Authorization': `Bearer xxxx`,
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ module.exports = {
|
|||||||
modifyVars: {
|
modifyVars: {
|
||||||
...(process.env.NODE_ENV === "development"
|
...(process.env.NODE_ENV === "development"
|
||||||
? { "@primary-color": "#a51d1d" }
|
? { "@primary-color": "#a51d1d" }
|
||||||
: { "@primary-color": "#1DA57A" }),
|
: {
|
||||||
|
//"@primary-color": "#1DA57A"
|
||||||
|
}),
|
||||||
// "@primary-color": " #1890ff", // primary color for all components
|
// "@primary-color": " #1890ff", // primary color for all components
|
||||||
// "@link-color": "#1890ff", // link color
|
// "@link-color": "#1890ff", // link color
|
||||||
// "@success-color": "#52c41a", // success state color
|
// "@success-color": "#52c41a", // success state color
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 613 B |
42958
client/package-lock.json
generated
@@ -5,7 +5,7 @@
|
|||||||
"proxy": "http://localhost:5000",
|
"proxy": "http://localhost:5000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.17",
|
"@apollo/client": "^3.3.17",
|
||||||
"@craco/craco": "^6.1.2",
|
"@craco/craco": "^5.9.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.1.2",
|
"@fingerprintjs/fingerprintjs": "^3.1.2",
|
||||||
"@lourenci/react-kanban": "^2.1.0",
|
"@lourenci/react-kanban": "^2.1.0",
|
||||||
"@sentry/react": "^6.3.6",
|
"@sentry/react": "^6.3.6",
|
||||||
@@ -19,7 +19,9 @@
|
|||||||
"craco-less": "^1.17.1",
|
"craco-less": "^1.17.1",
|
||||||
"dinero.js": "^1.8.1",
|
"dinero.js": "^1.8.1",
|
||||||
"dotenv": "^9.0.2",
|
"dotenv": "^9.0.2",
|
||||||
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
|
"exifr": "^7.0.0",
|
||||||
"firebase": "^8.6.0",
|
"firebase": "^8.6.0",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.5.0",
|
||||||
"i18next": "^20.2.2",
|
"i18next": "^20.2.2",
|
||||||
@@ -28,17 +30,21 @@
|
|||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.9.17",
|
"libphonenumber-js": "^1.9.17",
|
||||||
"logrocket": "^1.2.0",
|
"logrocket": "^1.2.0",
|
||||||
|
"markerjs2": "^2.8.1",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"phone": "^2.4.21",
|
"phone": "^2.4.21",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.0",
|
||||||
|
"rc-queue-anim": "^1.8.5",
|
||||||
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-big-calendar": "^0.33.2",
|
"react-big-calendar": "^0.33.2",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-drag-listview": "^0.1.8",
|
"react-drag-listview": "^0.1.8",
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^0.5.5",
|
||||||
|
"react-grid-layout": "^1.2.5",
|
||||||
"react-i18next": "^11.8.15",
|
"react-i18next": "^11.8.15",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-number-format": "^4.5.5",
|
"react-number-format": "^4.5.5",
|
||||||
@@ -46,6 +52,7 @@
|
|||||||
"react-resizable": "^3.0.1",
|
"react-resizable": "^3.0.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.3",
|
||||||
"recharts": "^2.0.7",
|
"recharts": "^2.0.7",
|
||||||
"redux": "^4.1.0",
|
"redux": "^4.1.0",
|
||||||
@@ -54,6 +61,7 @@
|
|||||||
"redux-state-sync": "^3.1.2",
|
"redux-state-sync": "^3.1.2",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"sass": "^1.32.13",
|
"sass": "^1.32.13",
|
||||||
|
"socket.io-client": "^4.1.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
"web-vitals": "^1.1.2",
|
"web-vitals": "^1.1.2",
|
||||||
@@ -74,8 +82,8 @@
|
|||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||||
"build:test": "env-cmd -f .env.test npm run build",
|
"build:test": "env-cmd -f .env.test yarn run build",
|
||||||
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
"build-deploy:test": "yarn 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",
|
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` react-scripts build",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
|
|||||||
@@ -8,13 +8,61 @@
|
|||||||
<meta name="description" content="ImEX Online" />
|
<meta name="description" content="ImEX Online" />
|
||||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||||
<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
|
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/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import enLocale from "antd/es/locale/en_US";
|
|||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
moment.locale("en-US");
|
moment.locale("en-US");
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
||||||
|
|
||||||
export default function AppContainer() {
|
export default function AppContainer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
|
import { Button, Result } from "antd";
|
||||||
import React, { lazy, Suspense, useEffect } from "react";
|
import React, { lazy, Suspense, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||||
//Component Imports
|
//Component Imports
|
||||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||||
import AboutPage from "../pages/about/about.page";
|
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
||||||
import TechPageContainer from "../pages/tech/tech.page.container";
|
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 { checkUserSession } from "../redux/user/user.actions";
|
||||||
import { selectCurrentUser } from "../redux/user/user.selectors";
|
import { selectCurrentUser } from "../redux/user/user.selectors";
|
||||||
import PrivateRoute from "../utils/private-route";
|
import PrivateRoute from "../utils/private-route";
|
||||||
import "./App.styles.scss";
|
import "./App.styles.scss";
|
||||||
|
|
||||||
const LandingPage = lazy(() => import("../pages/landing/landing.page"));
|
import LandingPage from "../pages/landing/landing.page";
|
||||||
const ResetPassword = lazy(() =>
|
const ResetPassword = lazy(() =>
|
||||||
import("../pages/reset-password/reset-password.component")
|
import("../pages/reset-password/reset-password.component")
|
||||||
);
|
);
|
||||||
@@ -27,25 +31,59 @@ const MobilePaymentContainer = lazy(() =>
|
|||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
|
online: selectOnline,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession()),
|
checkUserSession: () => dispatch(checkUserSession()),
|
||||||
|
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function App({ checkUserSession, currentUser }) {
|
export function App({ checkUserSession, currentUser, online, setOnline }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
setOnline(false);
|
||||||
|
}
|
||||||
|
|
||||||
checkUserSession();
|
checkUserSession();
|
||||||
}, [checkUserSession]);
|
}, [checkUserSession, setOnline]);
|
||||||
|
|
||||||
//const b = Grid.useBreakpoint();
|
//const b = Grid.useBreakpoint();
|
||||||
// console.log("Breakpoints:", b);
|
// console.log("Breakpoints:", b);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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) {
|
if (currentUser.authorized === null) {
|
||||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
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 (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
|
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
|
||||||
@@ -62,7 +100,7 @@ export function App({ checkUserSession, currentUser }) {
|
|||||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Route exact path="/about" component={AboutPage} />
|
<Route exact path="/disclaimer" component={DisclaimerPage} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Route
|
<Route
|
||||||
@@ -85,6 +123,13 @@ export function App({ checkUserSession, currentUser }) {
|
|||||||
component={TechPageContainer}
|
component={TechPageContainer}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<PrivateRoute
|
||||||
|
isAuthorized={currentUser.authorized}
|
||||||
|
path="/edit"
|
||||||
|
component={DocumentEditorContainer}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,3 +118,9 @@
|
|||||||
.production-list-min-height {
|
.production-list-min-height {
|
||||||
min-height: 19px;
|
min-height: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#noticeable-widget {
|
||||||
|
iframe {
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
client/src/assets/ImEX Online Logo - Dark.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
client/src/assets/ImEX Online Logo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
client/src/assets/banner-logo.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
client/src/assets/banner1.jpeg
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
client/src/assets/banner2.jpeg
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
client/src/assets/banner3.jpeg
Normal file
|
After Width: | Height: | Size: 177 KiB |
5
client/src/assets/icons/technology.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -5,9 +5,9 @@ import { Link } from "react-router-dom";
|
|||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
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({
|
export default function AccountingPayablesTableComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -41,19 +41,12 @@ export default function AccountingPayablesTableComponent({
|
|||||||
title: t("payments.fields.date"),
|
title: t("payments.fields.date"),
|
||||||
dataIndex: "date",
|
dataIndex: "date",
|
||||||
key: "date",
|
key: "date",
|
||||||
sorter: (a, b) => alphaSort(a.date, b.date),
|
sorter: (a, b) => dateSort(a.date, b.date),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
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"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner",
|
dataIndex: "owner",
|
||||||
@@ -61,7 +54,7 @@ export default function AccountingPayablesTableComponent({
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "ownr_ln" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.job.owner ? (
|
return record.job.owner ? (
|
||||||
<Link to={"/manage/owners/" + record.job.owner.id}>
|
<Link to={"/manage/owners/" + record.job.owner.id}>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export function BillFormComponent({
|
|||||||
loadLines,
|
loadLines,
|
||||||
billEdit,
|
billEdit,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
|
job,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -50,6 +51,10 @@ export function BillFormComponent({
|
|||||||
setDiscount(opt.discount);
|
setDiscount(opt.discount);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (job) form.validateFields(["is_credit_memo"]);
|
||||||
|
}, [job, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
||||||
const vendorId = form.getFieldValue("vendorid");
|
const vendorId = form.getFieldValue("vendorid");
|
||||||
@@ -89,7 +94,7 @@ export function BillFormComponent({
|
|||||||
<JobSearchSelect
|
<JobSearchSelect
|
||||||
disabled={billEdit || disabled}
|
disabled={billEdit || disabled}
|
||||||
convertedOnly
|
convertedOnly
|
||||||
// notExported={false}
|
notExported={false}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (form.getFieldValue("jobid") !== null) {
|
if (form.getFieldValue("jobid") !== null) {
|
||||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||||
@@ -106,6 +111,18 @@ export function BillFormComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
//message: t("general.validation.required"),
|
//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
|
<VendorSearchSelect
|
||||||
@@ -175,6 +192,22 @@ export function BillFormComponent({
|
|||||||
label={t("bills.fields.is_credit_memo")}
|
label={t("bills.fields.is_credit_memo")}
|
||||||
name="is_credit_memo"
|
name="is_credit_memo"
|
||||||
valuePropName="checked"
|
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 />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function BillFormContainer({
|
|||||||
}
|
}
|
||||||
loadLines={loadLines}
|
loadLines={loadLines}
|
||||||
lineData={lineData ? lineData.joblines : []}
|
lineData={lineData ? lineData.joblines : []}
|
||||||
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
disableInvNumber={disableInvNumber}
|
disableInvNumber={disableInvNumber}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({ message: t("bills.successes.save") });
|
notification["success"]({
|
||||||
|
message: t("bills.successes.reexport"),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("bills.errors.saving", {
|
message: t("bills.errors.saving", {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EyeFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -47,7 +47,7 @@ export function BillsListTableComponent({
|
|||||||
<Space wrap>
|
<Space wrap>
|
||||||
{showView && (
|
{showView && (
|
||||||
<Button onClick={() => handleOnRowClick(record)}>
|
<Button onClick={() => handleOnRowClick(record)}>
|
||||||
<EyeFilled />
|
<EditFilled />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<BillDeleteButton bill={record} />
|
<BillDeleteButton bill={record} />
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { HomeFilled } from "@ant-design/icons";
|
import { HomeFilled } from "@ant-design/icons";
|
||||||
import { Breadcrumb } from "antd";
|
import { Breadcrumb, Row, Col } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||||
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
import "./breadcrumbs.styles.scss";
|
import "./breadcrumbs.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -13,24 +14,29 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
export function BreadCrumbs({ breadcrumbs }) {
|
export function BreadCrumbs({ breadcrumbs }) {
|
||||||
return (
|
return (
|
||||||
<div className="breadcrumb-container imex-flex-row">
|
<Row className="breadcrumb-container">
|
||||||
<Breadcrumb separator=">">
|
<Col xs={24} sm={24} md={16}>
|
||||||
<Breadcrumb.Item>
|
<Breadcrumb separator=">">
|
||||||
<Link to={`/manage`}>
|
<Breadcrumb.Item>
|
||||||
<HomeFilled />
|
<Link to={`/manage`}>
|
||||||
</Link>
|
<HomeFilled />
|
||||||
</Breadcrumb.Item>
|
</Link>
|
||||||
{breadcrumbs.map((item) =>
|
</Breadcrumb.Item>
|
||||||
item.link ? (
|
{breadcrumbs.map((item) =>
|
||||||
<Breadcrumb.Item key={item.label}>
|
item.link ? (
|
||||||
<Link to={item.link}>{item.label} </Link>
|
<Breadcrumb.Item key={item.label}>
|
||||||
</Breadcrumb.Item>
|
<Link to={item.link}>{item.label} </Link>
|
||||||
) : (
|
</Breadcrumb.Item>
|
||||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
) : (
|
||||||
)
|
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
||||||
)}
|
)
|
||||||
</Breadcrumb>
|
)}
|
||||||
</div>
|
</Breadcrumb>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={8}>
|
||||||
|
<GlobalSearch />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { useSubscription } from "@apollo/client";
|
import { useSubscription } from "@apollo/client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERSATION_LIST_SUBSCRIPTION } from "../../graphql/conversations.queries";
|
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 AlertComponent from "../alert/alert.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import ChatAffixComponent from "./chat-affix.component";
|
import ChatAffixComponent from "./chat-affix.component";
|
||||||
import { Affix } from "antd";
|
|
||||||
import "./chat-affix.styles.scss";
|
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({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
chatVisible: selectChatVisible,
|
chatVisible: selectChatVisible,
|
||||||
@@ -31,22 +29,20 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Affix className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||||
<div>
|
{bodyshop && bodyshop.messagingservicesid ? (
|
||||||
{bodyshop && bodyshop.messagingservicesid ? (
|
<ChatAffixComponent
|
||||||
<ChatAffixComponent
|
conversationList={(data && data.conversations) || []}
|
||||||
conversationList={(data && data.conversations) || []}
|
unreadCount={
|
||||||
unreadCount={
|
(data &&
|
||||||
(data &&
|
data.conversations.reduce((acc, val) => {
|
||||||
data.conversations.reduce((acc, val) => {
|
return (acc = acc + val.messages_aggregate.aggregate.count);
|
||||||
return (acc = acc + val.messages_aggregate.aggregate.count);
|
}, 0)) ||
|
||||||
}, 0)) ||
|
0
|
||||||
0
|
}
|
||||||
}
|
/>
|
||||||
/>
|
) : null}
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
|
||||||
</Affix>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
.chat-affix {
|
.chat-affix {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
|
left: 2vw;
|
||||||
bottom: 2vh;
|
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 {
|
.chat-affix-open {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
|
import { Space } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
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 ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
|
||||||
export default function ChatConversationTitle({ conversation }) {
|
export default function ChatConversationTitle({ conversation }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Space wrap>
|
||||||
<div className="imex-flex-row">
|
<PhoneNumberFormatter>
|
||||||
<ChatConversationTitleTags
|
{conversation && conversation.phone_num}
|
||||||
jobConversations={
|
</PhoneNumberFormatter>
|
||||||
(conversation && conversation.job_conversations) || []
|
<ChatConversationTitleTags
|
||||||
}
|
jobConversations={
|
||||||
/>
|
(conversation && conversation.job_conversations) || []
|
||||||
<ChatTagRoContainer conversation={conversation || []} />
|
}
|
||||||
</div>
|
/>
|
||||||
<div className="imex-flex-row">
|
<ChatTagRoContainer conversation={conversation || []} />
|
||||||
<PhoneNumberFormatter>
|
<ChatArchiveButton conversation={conversation} />
|
||||||
{conversation && conversation.phone_num}
|
</Space>
|
||||||
</PhoneNumberFormatter>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
import { CloseCircleOutlined, LoadingOutlined } from "@ant-design/icons";
|
||||||
import { Select, Empty } from "antd";
|
import { Select, Empty, Space } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -13,27 +13,27 @@ export default function ChatTagRoComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Space flex>
|
||||||
<Select
|
<div style={{ width: "15rem" }}>
|
||||||
showSearch
|
<Select
|
||||||
autoFocus
|
showSearch
|
||||||
style={{
|
autoFocus
|
||||||
width: 300,
|
dropdownMatchSelectWidth
|
||||||
}}
|
placeholder={t("general.labels.search")}
|
||||||
placeholder={t("general.labels.search")}
|
filterOption={false}
|
||||||
filterOption={false}
|
onSearch={handleSearch}
|
||||||
onSearch={handleSearch}
|
onSelect={handleInsertTag}
|
||||||
onSelect={handleInsertTag}
|
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
>
|
||||||
>
|
{roOptions.map((item, idx) => (
|
||||||
{roOptions.map((item, idx) => (
|
<Select.Option key={item.id || idx}>
|
||||||
<Select.Option key={item.id || idx}>
|
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
||||||
{` ${item.ro_number || ""} | ${item.ownr_fn || ""} ${
|
item.ownr_ln || ""
|
||||||
item.ownr_ln || ""
|
} ${item.ownr_co_nm || ""}`}
|
||||||
} ${item.ownr_co_nm || ""}`}
|
</Select.Option>
|
||||||
</Select.Option>
|
))}
|
||||||
))}
|
</Select>
|
||||||
</Select>
|
</div>
|
||||||
{loading ? <LoadingOutlined /> : null}
|
{loading ? <LoadingOutlined /> : null}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -41,6 +41,6 @@ export default function ChatTagRoComponent({
|
|||||||
) : (
|
) : (
|
||||||
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function PartsReceiveModalComponent({ bodyshop, form }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Form.Item name="fleet" label={t("courtesycars.fields.fleetnumber")}>
|
<Form.Item name="plate" label={t("courtesycars.fields.plate")}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export function ContractsFindModalContainer({
|
|||||||
|
|
||||||
callSearch({
|
callSearch({
|
||||||
variables: {
|
variables: {
|
||||||
fleet:
|
plate:
|
||||||
(values.fleet && values.fleet !== "" && values.fleet) || undefined,
|
(values.plate && values.plate !== "" && values.plate) || undefined,
|
||||||
time: values.time,
|
time: values.time,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,13 +90,12 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
// sorter: (a, b) => alphaSort(a.model, b.model),
|
// sorter: (a, b) => alphaSort(a.model, b.model),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) =>
|
||||||
<div>
|
record.cccontracts.length === 1 ? (
|
||||||
{record.cccontracts.length === 1
|
<Link to={`/manage/jobs/${record.cccontracts[0].job.id}`}>
|
||||||
? record.cccontracts[0].job.ro_number
|
{record.cccontracts[0].job.ro_number}
|
||||||
: null}
|
</Link>
|
||||||
</div>
|
) : null,
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
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>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,29 +2,30 @@ import { Card } from "antd";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import _ from "lodash";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
Bar,
|
Bar,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
ComposedChart,
|
ComposedChart,
|
||||||
Legend,
|
Legend,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
import * as Utils from "../../scoreboard-targets-table/scoreboard-targets-table.util";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
if (!data) return null;
|
||||||
|
if (!data.monthly_sales) return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
const jobsByDate = {
|
const jobsByDate = _.groupBy(data.monthly_sales, (item) =>
|
||||||
"2020-07-5": [{ clm_total: 1224 }],
|
moment(item.date_invoiced).format("YYYY-MM-DD")
|
||||||
"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();
|
const listOfDays = Utils.ListOfDaysInCurrentMonth();
|
||||||
|
|
||||||
@@ -33,17 +34,19 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
|||||||
let dailySales;
|
let dailySales;
|
||||||
if (!!jobsByDate[val]) {
|
if (!!jobsByDate[val]) {
|
||||||
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||||
return dayAcc + dayVal.clm_total;
|
return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
|
||||||
}, 0);
|
}, Dinero());
|
||||||
} else {
|
} else {
|
||||||
dailySales = 0;
|
dailySales = Dinero();
|
||||||
}
|
}
|
||||||
|
|
||||||
const theValue = {
|
const theValue = {
|
||||||
date: moment(val).format("D dd"),
|
date: moment(val).format("DD"),
|
||||||
dailySales,
|
dailySales: dailySales.getAmount() / 100,
|
||||||
accSales:
|
accSales:
|
||||||
acc.length > 0 ? acc[acc.length - 1].accSales + dailySales : dailySales,
|
acc.length > 0
|
||||||
|
? acc[acc.length - 1].accSales + dailySales.getAmount() / 100
|
||||||
|
: dailySales.getAmount() / 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
return [...acc, theValue];
|
return [...acc, theValue];
|
||||||
@@ -51,32 +54,40 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
<Card title={t("dashboard.titles.monthlyrevenuegraph")} {...cardProps}>
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<div style={{ height: "100%" }}>
|
||||||
<ComposedChart
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
data={chartData}
|
<ComposedChart
|
||||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
data={chartData}
|
||||||
>
|
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||||
<CartesianGrid stroke="#f5f5f5" />
|
>
|
||||||
<XAxis dataKey="date" />
|
<CartesianGrid stroke="#f5f5f5" />
|
||||||
<YAxis />
|
<XAxis dataKey="date" />
|
||||||
<Tooltip />
|
<YAxis />
|
||||||
<Legend />
|
<Tooltip
|
||||||
<Area
|
formatter={(value, name, props) => value && value.toFixed(2)}
|
||||||
type="monotone"
|
/>
|
||||||
name="Accumulated Sales"
|
<Legend />
|
||||||
dataKey="accSales"
|
<Area
|
||||||
fill="#8884d8"
|
type="monotone"
|
||||||
stroke="#8884d8"
|
name="Accumulated Sales"
|
||||||
/>
|
dataKey="accSales"
|
||||||
<Bar
|
fill="#3CB371"
|
||||||
name="Daily Sales"
|
stroke="#3CB371"
|
||||||
dataKey="dailySales"
|
/>
|
||||||
//stackId="day"
|
<Bar
|
||||||
barSize={20}
|
name="Daily Sales"
|
||||||
fill="#413ea0"
|
dataKey="dailySales"
|
||||||
/>
|
//stackId="day"
|
||||||
</ComposedChart>
|
barSize={20}
|
||||||
</ResponsiveContainer>
|
fill="#413ea0"
|
||||||
|
/>
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DashboardMonthlyRevenueGraphGql = `
|
||||||
|
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,30 +1,40 @@
|
|||||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
|
||||||
import { Card, Statistic } from "antd";
|
import { Card, Statistic } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const aboveTargetMonthlySales = false;
|
if (!data) return null;
|
||||||
|
if (!data.projected_monthly_sales)
|
||||||
|
return <DashboardRefreshRequired {...cardProps} />;
|
||||||
|
|
||||||
|
const dollars =
|
||||||
|
data.projected_monthly_sales &&
|
||||||
|
data.projected_monthly_sales.reduce(
|
||||||
|
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
||||||
|
Dinero()
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Card {...cardProps}>
|
<Card title={t("dashboard.titles.projectedmonthlysales")} {...cardProps}>
|
||||||
<Statistic
|
<Statistic value={dollars.toFormat()} />
|
||||||
title={t("dashboard.titles.projectedmonthlysales")}
|
|
||||||
value={222000.0}
|
|
||||||
precision={2}
|
|
||||||
prefix={
|
|
||||||
<div>
|
|
||||||
{aboveTargetMonthlySales ? (
|
|
||||||
<ArrowUpOutlined />
|
|
||||||
) : (
|
|
||||||
<ArrowDownOutlined />
|
|
||||||
)}
|
|
||||||
$
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
valueStyle={{ color: aboveTargetMonthlySales ? "green" : "red" }}
|
|
||||||
/>
|
|
||||||
</Card>
|
</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
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,33 +1,26 @@
|
|||||||
import React from "react";
|
|
||||||
import { Card, Statistic } from "antd";
|
import { Card, Statistic } from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
export default function DashboardTotalProductionDollars({
|
export default function DashboardTotalProductionDollars({
|
||||||
data,
|
data,
|
||||||
...cardProps
|
...cardProps
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const aboveTargetProductionDollars = false;
|
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.totals.subtotal)),
|
||||||
|
Dinero()
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card {...cardProps}>
|
<Card title={t("dashboard.labels.dollarsinproduction")} {...cardProps}>
|
||||||
<Statistic
|
<Statistic value={dollars.toFormat()} />
|
||||||
title={t("dashboard.titles.productiondollars")}
|
|
||||||
value={175000.0}
|
|
||||||
precision={2}
|
|
||||||
prefix={
|
|
||||||
<div>
|
|
||||||
{aboveTargetProductionDollars ? (
|
|
||||||
<ArrowUpOutlined />
|
|
||||||
) : (
|
|
||||||
<ArrowDownOutlined />
|
|
||||||
)}
|
|
||||||
$
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
valueStyle={{ color: aboveTargetProductionDollars ? "green" : "red" }}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,63 @@
|
|||||||
|
import { Card, Space, Statistic } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Card, Statistic } from "antd";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ArrowDownOutlined, ArrowUpOutlined } from "@ant-design/icons";
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||||
|
import DashboardRefreshRequired from "../refresh-required.component";
|
||||||
|
|
||||||
export default function DashboardTotalProductionHours({ data, ...cardProps }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DashboardTotalProductionHours);
|
||||||
|
|
||||||
|
export function DashboardTotalProductionHours({
|
||||||
|
bodyshop,
|
||||||
|
data,
|
||||||
|
...cardProps
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const aboveTargetHours = true;
|
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;
|
||||||
return (
|
return (
|
||||||
<Card {...cardProps}>
|
<Card {...cardProps} title={t("dashboard.titles.prodhrssummary")}>
|
||||||
<Statistic
|
<Space wrap style={{ flex: 1 }}>
|
||||||
title={t("dashboard.titles.productionhours")}
|
<Statistic
|
||||||
value={750}
|
title={t("dashboard.labels.bodyhrs")}
|
||||||
prefix={aboveTargetHours ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
|
value={hours.body.toFixed(1)}
|
||||||
valueStyle={{ color: aboveTargetHours ? "green" : "red" }}
|
/>
|
||||||
/>
|
<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>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DashboardTotalProductionHoursGql = ``;
|
||||||
|
|||||||
@@ -1,185 +1,355 @@
|
|||||||
// import Icon from "@ant-design/icons";
|
import Icon, { SyncOutlined } from "@ant-design/icons";
|
||||||
// import { Button, Dropdown, Menu, notification } from "antd";
|
import { gql, useMutation, useQuery } from "@apollo/client";
|
||||||
// import React, { useState } from "react";
|
import { Button, Dropdown, Menu, notification, PageHeader, Space } from "antd";
|
||||||
// import { useMutation, useQuery } from "@apollo/client";
|
import i18next from "i18next";
|
||||||
// import { Responsive, WidthProvider } from "react-grid-layout";
|
import _ from "lodash";
|
||||||
// import { useTranslation } from "react-i18next";
|
import moment from "moment";
|
||||||
// import { MdClose } from "react-icons/md";
|
import React, { useState } from "react";
|
||||||
// import { connect } from "react-redux";
|
import { Responsive, WidthProvider } from "react-grid-layout";
|
||||||
// import { createStructuredSelector } from "reselect";
|
import { useTranslation } from "react-i18next";
|
||||||
// import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { MdClose } from "react-icons/md";
|
||||||
// import { QUERY_DASHBOARD_DETAILS } from "../../graphql/bodyshop.queries";
|
import { connect } from "react-redux";
|
||||||
// import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
import { createStructuredSelector } from "reselect";
|
||||||
// import {
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
// selectBodyshop,
|
import { UPDATE_DASHBOARD_LAYOUT } from "../../graphql/user.queries";
|
||||||
// selectCurrentUser,
|
import {
|
||||||
// } from "../../redux/user/user.selectors";
|
selectBodyshop,
|
||||||
// import AlertComponent from "../alert/alert.component";
|
selectCurrentUser,
|
||||||
// import DashboardMonthlyRevenueGraph from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component";
|
} from "../../redux/user/user.selectors";
|
||||||
// import DashboardProjectedMonthlySales from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
// import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component";
|
import DashboardMonthlyEmployeeEfficiency, {
|
||||||
// import DashboardTotalProductionHours from "../dashboard-components/total-production-hours/total-production-hours.component";
|
DashboardMonthlyEmployeeEfficiencyGql,
|
||||||
// import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component";
|
||||||
// //Combination of the following:
|
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component";
|
||||||
// // /node_modules/react-grid-layout/css/styles.css
|
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component";
|
||||||
// // /node_modules/react-resizable/css/styles.css
|
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component";
|
||||||
// import "./dashboard-grid.styles.css";
|
import DashboardMonthlyRevenueGraph, {
|
||||||
// import "./dashboard-grid.styles.scss";
|
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";
|
||||||
|
|
||||||
// const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
const ResponsiveReactGridLayout = WidthProvider(Responsive);
|
||||||
|
|
||||||
// const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
// currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
// bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
// });
|
});
|
||||||
// const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
// //setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
// });
|
});
|
||||||
|
|
||||||
// export function DashboardGridComponent({ currentUser, bodyshop }) {
|
export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||||
// const { loading, error, data } = useQuery(QUERY_DASHBOARD_DETAILS);
|
const { t } = useTranslation();
|
||||||
// const { t } = useTranslation();
|
const [state, setState] = useState({
|
||||||
// const [state, setState] = useState({
|
...(bodyshop.associations[0].user.dashboardlayout
|
||||||
// layout: bodyshop.associations[0].user.dashboardlayout || [
|
? bodyshop.associations[0].user.dashboardlayout
|
||||||
// { i: "ProductionDollars", x: 0, y: 0, w: 2, h: 2 },
|
: { items: [], layout: {}, layouts: [] }),
|
||||||
// // { i: "ProductionHours", x: 2, y: 0, w: 2, h: 2 },
|
});
|
||||||
// ],
|
|
||||||
// });
|
|
||||||
// const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
|
||||||
|
|
||||||
// const handleLayoutChange = async (newLayout) => {
|
const { loading, error, data, refetch } = useQuery(
|
||||||
// logImEXEvent("dashboard_change_layout");
|
createDashboardQuery(state)
|
||||||
// setState({ ...state, layout: newLayout });
|
);
|
||||||
// const result = await updateLayout({
|
|
||||||
// variables: { email: currentUser.email, layout: newLayout },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!!result.errors) {
|
const [updateLayout] = useMutation(UPDATE_DASHBOARD_LAYOUT);
|
||||||
// notification["error"]({
|
|
||||||
// message: t("dashboard.errors.updatinglayout", {
|
|
||||||
// message: JSON.stringify(result.errors),
|
|
||||||
// }),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleRemoveComponent = (key) => {
|
const handleLayoutChange = async (layout, layouts) => {
|
||||||
// logImEXEvent("dashboard_remove_component", { name: key });
|
logImEXEvent("dashboard_change_layout");
|
||||||
|
|
||||||
// const idxToRemove = state.layout.findIndex((i) => i.i === key);
|
setState({ ...state, layout, layouts });
|
||||||
// const newLayout = state.layout;
|
|
||||||
// newLayout.splice(idxToRemove, 1);
|
|
||||||
// handleLayoutChange(newLayout);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleAddComponent = (e) => {
|
const result = await updateLayout({
|
||||||
// logImEXEvent("dashboard_add_component", { name: e });
|
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);
|
||||||
|
|
||||||
// handleLayoutChange([
|
items.splice(idxToRemove, 1);
|
||||||
// ...state.layout,
|
setState({ ...state, items });
|
||||||
// {
|
};
|
||||||
// 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 onBreakpointChange = (breakpoint, cols) => {
|
const handleAddComponent = (e) => {
|
||||||
// setState({ ...state, breakpoint: breakpoint, cols: cols });
|
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 existingLayoutKeys = state.layout.map((i) => i.i);
|
const dashboarddata = React.useMemo(
|
||||||
// const addComponentOverlay = (
|
() => GenerateDashboardData(data),
|
||||||
// <Menu onClick={handleAddComponent}>
|
[data]
|
||||||
// {Object.keys(componentList).map((key) => (
|
);
|
||||||
// <Menu.Item
|
const existingLayoutKeys = state.items.map((i) => i.i);
|
||||||
// key={key}
|
const addComponentOverlay = (
|
||||||
// value={key}
|
<Menu onClick={handleAddComponent}>
|
||||||
// disabled={existingLayoutKeys.includes(key)}
|
{Object.keys(componentList).map((key) => (
|
||||||
// >
|
<Menu.Item
|
||||||
// {componentList[key].label}
|
key={key}
|
||||||
// </Menu.Item>
|
value={key}
|
||||||
// ))}
|
disabled={existingLayoutKeys.includes(key)}
|
||||||
// </Menu>
|
>
|
||||||
// );
|
{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 (
|
return (
|
||||||
// <div>
|
<div>
|
||||||
// <Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
<PageHeader
|
||||||
// <Button>{t("dashboard.actions.addcomponent")}</Button>
|
extra={
|
||||||
// </Dropdown>
|
<Space>
|
||||||
// <ResponsiveReactGridLayout
|
<Button onClick={() => refetch()}>
|
||||||
// className="layout"
|
<SyncOutlined />
|
||||||
// breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
</Button>
|
||||||
// cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
<Dropdown overlay={addComponentOverlay} trigger={["click"]}>
|
||||||
// width="100%"
|
<Button>{t("dashboard.actions.addcomponent")}</Button>
|
||||||
// onLayoutChange={handleLayoutChange}
|
</Dropdown>
|
||||||
// onBreakpointChange={onBreakpointChange}
|
</Space>
|
||||||
// >
|
}
|
||||||
// {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>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default connect(
|
<ResponsiveReactGridLayout
|
||||||
// mapStateToProps,
|
className="layout"
|
||||||
// mapDispatchToProps
|
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||||
// )(DashboardGridComponent);
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// const componentList = {
|
export default connect(
|
||||||
// ProductionDollars: {
|
mapStateToProps,
|
||||||
// label: "Production Dollars",
|
mapDispatchToProps
|
||||||
// component: DashboardTotalProductionDollars,
|
)(DashboardGridComponent);
|
||||||
// w: 2,
|
|
||||||
// h: 1,
|
const componentList = {
|
||||||
// },
|
ProductionDollars: {
|
||||||
// ProductionHours: {
|
label: i18next.t("dashboard.titles.productiondollars"),
|
||||||
// label: "Production Hours",
|
component: DashboardTotalProductionDollars,
|
||||||
// component: DashboardTotalProductionHours,
|
gqlFragment: null,
|
||||||
// w: 2,
|
w: 1,
|
||||||
// h: 1,
|
h: 1,
|
||||||
// },
|
minW: 2,
|
||||||
// ProjectedMonthlySales: {
|
minH: 1,
|
||||||
// label: "Projected Monthly Sales",
|
},
|
||||||
// component: DashboardProjectedMonthlySales,
|
ProductionHours: {
|
||||||
// w: 2,
|
label: i18next.t("dashboard.titles.productionhours"),
|
||||||
// h: 1,
|
component: DashboardTotalProductionHours,
|
||||||
// },
|
gqlFragment: DashboardTotalProductionHoursGql,
|
||||||
// MonthlyRevenueGraph: {
|
w: 3,
|
||||||
// label: "Monthly Sales Graph",
|
h: 1,
|
||||||
// component: DashboardMonthlyRevenueGraph,
|
minW: 3,
|
||||||
// w: 2,
|
minH: 1,
|
||||||
// h: 2,
|
},
|
||||||
// },
|
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.productiondollars"),
|
||||||
|
component: DashboardMonthlyPartsSales,
|
||||||
|
gqlFragment: null,
|
||||||
|
minW: 2,
|
||||||
|
minH: 2,
|
||||||
|
w: 2,
|
||||||
|
h: 2,
|
||||||
|
},
|
||||||
|
MonthlyLaborSales: {
|
||||||
|
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,12 +1,154 @@
|
|||||||
.dashboard-card {
|
.react-resizable {
|
||||||
// background-color: green;
|
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%;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
// background-color: red;
|
height: 80%;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
// // background-color: red;
|
||||||
flex-direction: column;
|
// height: 90%;
|
||||||
align-items: center;
|
// width: 100%;
|
||||||
|
// padding: 8px;
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
}
|
||||||
|
.ant-spin-nested-loading {
|
||||||
|
height: 100%;
|
||||||
|
.ant-spin-container {
|
||||||
|
height: 100%;
|
||||||
|
.ant-table {
|
||||||
|
height: 100%;
|
||||||
|
.ant-table-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function GenerateDashboardData(data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Button, Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { socket } from "../../pages/dms/dms.container";
|
||||||
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
export default function DmsCustomerSelector() {
|
||||||
|
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 onOk = () => {
|
||||||
|
setVisible(false);
|
||||||
|
socket.emit("cdk-selected-customer", selectedCustomer);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("dms.fields.name1"),
|
||||||
|
dataIndex: ["name1", "fullName"],
|
||||||
|
key: "name1",
|
||||||
|
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("dms.fields.name2"),
|
||||||
|
dataIndex: ["name2", "fullName"],
|
||||||
|
key: "name2",
|
||||||
|
sorter: (a, b) => alphaSort(a.name2?.fullName, b.name2?.fullName),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("dms.fields.phone"),
|
||||||
|
dataIndex: ["contactInfo", "mainTelephoneNumber", "value"],
|
||||||
|
key: "phone",
|
||||||
|
render: (record, value) => (
|
||||||
|
<PhoneFormatter>
|
||||||
|
{record.contactInfo?.mainTelephoneNumber?.value}
|
||||||
|
</PhoneFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("dms.fields.address"),
|
||||||
|
//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 (
|
||||||
|
<Table
|
||||||
|
title={() => (
|
||||||
|
<div>
|
||||||
|
<Button onClick={onOk}>Select</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey={(record) => record.id.value}
|
||||||
|
dataSource={customerList}
|
||||||
|
//onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (props) => {
|
||||||
|
setSelectedCustomer(props.id.value);
|
||||||
|
},
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [selectedCustomer],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
//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);
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
import { INSERT_NEW_DOCUMENT } from "../../graphql/documents.queries";
|
||||||
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
import { axiosAuthInterceptorId } from "../../utils/CleanAxios";
|
||||||
import client from "../../utils/GraphQLClient";
|
import client from "../../utils/GraphQLClient";
|
||||||
|
import exifr from "exifr";
|
||||||
|
|
||||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||||
|
|
||||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||||
@@ -19,8 +21,13 @@ export const handleUpload = (ev, context) => {
|
|||||||
const { onError, onSuccess, onProgress } = ev;
|
const { onError, onSuccess, onProgress } = ev;
|
||||||
const { bodyshop, jobId } = context;
|
const { bodyshop, jobId } = context;
|
||||||
|
|
||||||
let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`;
|
const fileName = ev.file.name || ev.filename;
|
||||||
let extension = ev.file.name.split(".").pop();
|
|
||||||
|
let key = `${bodyshop.id}/${jobId}/${fileName.replace(
|
||||||
|
/\.[^/.]+$/,
|
||||||
|
""
|
||||||
|
)}-${new Date().getTime()}`;
|
||||||
|
let extension = fileName.split(".").pop();
|
||||||
uploadToCloudinary(
|
uploadToCloudinary(
|
||||||
key,
|
key,
|
||||||
extension,
|
extension,
|
||||||
@@ -85,6 +92,7 @@ export const uploadToCloudinary = async (
|
|||||||
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
|
|
||||||
@@ -122,6 +130,16 @@ export const uploadToCloudinary = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Insert the document with the matching key.
|
//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({
|
const documentInsert = await client.mutate({
|
||||||
mutation: INSERT_NEW_DOCUMENT,
|
mutation: INSERT_NEW_DOCUMENT,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -132,9 +150,10 @@ export const uploadToCloudinary = async (
|
|||||||
uploaded_by: uploaded_by,
|
uploaded_by: uploaded_by,
|
||||||
key: key,
|
key: key,
|
||||||
type: fileType,
|
type: fileType,
|
||||||
extension: extension,
|
extension: cloudinaryUploadResponse.data.format || extension,
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
size: cloudinaryUploadResponse.data.bytes || file.size,
|
size: cloudinaryUploadResponse.data.bytes || file.size,
|
||||||
|
takenat,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -166,6 +185,7 @@ export const uploadToCloudinary = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Also needs to be updated in media JS and mobile app.
|
||||||
export function DetermineFileType(filetype) {
|
export function DetermineFileType(filetype) {
|
||||||
if (!filetype) return "auto";
|
if (!filetype) return "auto";
|
||||||
else if (filetype.startsWith("image")) return "image";
|
else if (filetype.startsWith("image")) return "image";
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { UploadOutlined } from "@ant-design/icons";
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
import { Card, Divider, Form, Input, Select, Upload } from "antd";
|
import { Divider, Form, Input, Select, Tabs, Upload } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import EmailDocumentsComponent from "../email-documents/email-documents.component";
|
||||||
|
|
||||||
export default function EmailOverlayComponent({ form }) {
|
export default function EmailOverlayComponent({ form, selectedMediaState }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -52,34 +53,38 @@ export default function EmailOverlayComponent({ form }) {
|
|||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Card title={t("emails.labels.attachments")}>
|
<Tabs>
|
||||||
<Form.Item
|
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
|
||||||
name="fileList"
|
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
|
||||||
valuePropName="fileList"
|
</Tabs.TabPane>
|
||||||
getValueFromEvent={(e) => {
|
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
|
||||||
console.log("Upload event:", e);
|
<Form.Item
|
||||||
if (Array.isArray(e)) {
|
name="fileList"
|
||||||
return e;
|
valuePropName="fileList"
|
||||||
}
|
getValueFromEvent={(e) => {
|
||||||
return e && e.fileList;
|
if (Array.isArray(e)) {
|
||||||
}}
|
return e;
|
||||||
>
|
}
|
||||||
<Upload.Dragger
|
return e && e.fileList;
|
||||||
beforeUpload={Upload.LIST_IGNORE}
|
}}
|
||||||
multiple
|
|
||||||
listType="picture-card"
|
|
||||||
>
|
>
|
||||||
<>
|
<Upload.Dragger
|
||||||
<p className="ant-upload-drag-icon">
|
beforeUpload={Upload.LIST_IGNORE}
|
||||||
<UploadOutlined />
|
multiple
|
||||||
</p>
|
listType="picture-card"
|
||||||
<p className="ant-upload-text">
|
>
|
||||||
Click or drag files to this area to upload.
|
<>
|
||||||
</p>
|
<p className="ant-upload-drag-icon">
|
||||||
</>
|
<UploadOutlined />
|
||||||
</Upload.Dragger>
|
</p>
|
||||||
</Form.Item>
|
<p className="ant-upload-text">
|
||||||
</Card>
|
Click or drag files to this area to upload.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
</Upload.Dragger>
|
||||||
|
</Form.Item>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export function EmailOverlayContainer({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [rawHtml, setRawHtml] = useState("");
|
const [rawHtml, setRawHtml] = useState("");
|
||||||
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
|
||||||
const defaultEmailFrom = {
|
const defaultEmailFrom = {
|
||||||
from: {
|
from: {
|
||||||
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
|
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
|
||||||
@@ -56,17 +58,18 @@ export function EmailOverlayContainer({
|
|||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("email_send_from_modal");
|
logImEXEvent("email_send_from_modal");
|
||||||
console.log(`values`, values);
|
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
|
|
||||||
await asyncForEach(values.fileList, async (f) => {
|
if (values.fileList)
|
||||||
const t = {
|
await asyncForEach(values.fileList, async (f) => {
|
||||||
ContentType: f.type,
|
const t = {
|
||||||
Filename: f.name,
|
ContentType: f.type,
|
||||||
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
Filename: f.name,
|
||||||
};
|
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
||||||
attachments.push(t);
|
};
|
||||||
});
|
attachments.push(t);
|
||||||
|
});
|
||||||
|
|
||||||
setSending(true);
|
setSending(true);
|
||||||
try {
|
try {
|
||||||
@@ -74,9 +77,12 @@ export function EmailOverlayContainer({
|
|||||||
...defaultEmailFrom,
|
...defaultEmailFrom,
|
||||||
...values,
|
...values,
|
||||||
html: rawHtml,
|
html: rawHtml,
|
||||||
attachments: await Promise.all(
|
attachments:
|
||||||
values.fileList.map(async (f) => await toBase64(f.originFileObj))
|
values.fileList &&
|
||||||
),
|
(await Promise.all(
|
||||||
|
values.fileList.map(async (f) => await toBase64(f.originFileObj))
|
||||||
|
)),
|
||||||
|
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
|
||||||
//attachments,
|
//attachments,
|
||||||
});
|
});
|
||||||
notification["success"]({ message: t("emails.successes.sent") });
|
notification["success"]({ message: t("emails.successes.sent") });
|
||||||
@@ -137,7 +143,12 @@ export function EmailOverlayContainer({
|
|||||||
<LoadingSpinner message={t("emails.labels.generatingemail")} />
|
<LoadingSpinner message={t("emails.labels.generatingemail")} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && <EmailOverlayComponent form={form} />}
|
{!loading && (
|
||||||
|
<EmailOverlayComponent
|
||||||
|
form={form}
|
||||||
|
selectedMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
@@ -34,21 +39,37 @@ class ErrorBoundary extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleErrorSubmit = () => {
|
handleErrorSubmit = () => {
|
||||||
const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
|
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
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
----
|
window.$crisp.push(["do", "chat:open"]);
|
||||||
System Generated Log:
|
// const errorDescription = `**Please add relevant details about what you were doing before you encountered this issue**
|
||||||
${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
|
// System Generated Log:
|
||||||
)}&customfield_10049=${window.location}&email=${
|
// ${this.state.error.message}
|
||||||
this.props.currentUser.email
|
// ${this.state.error.stack}
|
||||||
}`;
|
// `;
|
||||||
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() {
|
render() {
|
||||||
@@ -57,6 +78,23 @@ ${this.state.error.stack}
|
|||||||
if (this.state.hasErrored === true) {
|
if (this.state.hasErrored === true) {
|
||||||
logImEXEvent("error_boundary_rendered", { error, info });
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Result
|
<Result
|
||||||
@@ -74,7 +112,7 @@ ${this.state.error.stack}
|
|||||||
{t("general.actions.refresh")}
|
{t("general.actions.refresh")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={this.handleErrorSubmit}>
|
<Button onClick={this.handleErrorSubmit}>
|
||||||
{t("general.actions.submitticket")}
|
{t("general.actions.senderrortosupport")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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
|
||||||
|
*/
|
||||||
@@ -11,9 +11,8 @@ import AlertComponent from "../alert/alert.component";
|
|||||||
export default function GlobalSearch() {
|
export default function GlobalSearch() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [callSearch, { loading, error, data }] = useLazyQuery(
|
const [callSearch, { loading, error, data }] =
|
||||||
GLOBAL_SEARCH_QUERY
|
useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||||
);
|
|
||||||
|
|
||||||
const executeSearch = (v) => {
|
const executeSearch = (v) => {
|
||||||
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
|
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
|
||||||
@@ -38,7 +37,7 @@ export default function GlobalSearch() {
|
|||||||
value: job.ro_number,
|
value: job.ro_number,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/jobs/${job.id}`}>
|
<Link to={`/manage/jobs/${job.id}`}>
|
||||||
<Space wrap split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
<strong>{job.ro_number || t("general.labels.na")}</strong>
|
<strong>{job.ro_number || t("general.labels.na")}</strong>
|
||||||
<span>{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
|
<span>{`${job.ownr_fn || ""} ${job.ownr_ln || ""} ${
|
||||||
job.ownr_co_nm || ""
|
job.ownr_co_nm || ""
|
||||||
@@ -63,7 +62,7 @@ export default function GlobalSearch() {
|
|||||||
}`,
|
}`,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/owners/${owner.id}`}>
|
<Link to={`/manage/owners/${owner.id}`}>
|
||||||
<Space wrap split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
|
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
|
||||||
owner.ownr_co_nm || ""
|
owner.ownr_co_nm || ""
|
||||||
}`}</span>
|
}`}</span>
|
||||||
@@ -86,7 +85,7 @@ export default function GlobalSearch() {
|
|||||||
} ${vehicle.v_model_desc || ""}`,
|
} ${vehicle.v_model_desc || ""}`,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/vehicles/${vehicle.id}`}>
|
<Link to={`/manage/vehicles/${vehicle.id}`}>
|
||||||
<Space wrap split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
<span>
|
<span>
|
||||||
{`${vehicle.v_model_yr || ""} ${
|
{`${vehicle.v_model_yr || ""} ${
|
||||||
vehicle.v_make_desc || ""
|
vehicle.v_make_desc || ""
|
||||||
@@ -108,7 +107,7 @@ export default function GlobalSearch() {
|
|||||||
value: `${payment.job.ro_number} ${payment.payer} ${payment.amount}`,
|
value: `${payment.job.ro_number} ${payment.payer} ${payment.amount}`,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/jobs/${payment.job.id}`}>
|
<Link to={`/manage/jobs/${payment.job.id}`}>
|
||||||
<Space wrap split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
<span>{payment.job.ro_number}</span>
|
<span>{payment.job.ro_number}</span>
|
||||||
<span>{payment.job.memo}</span>
|
<span>{payment.job.memo}</span>
|
||||||
<span>{payment.job.amount}</span>
|
<span>{payment.job.amount}</span>
|
||||||
@@ -127,7 +126,7 @@ export default function GlobalSearch() {
|
|||||||
value: `${bill.invoice_number} - ${bill.vendor.name}`,
|
value: `${bill.invoice_number} - ${bill.vendor.name}`,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/bills?billid=${bill.id}`}>
|
<Link to={`/manage/bills?billid=${bill.id}`}>
|
||||||
<Space wrap split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
<span>{bill.invoice_number}</span>
|
<span>{bill.invoice_number}</span>
|
||||||
<span>{bill.vendor.name}</span>
|
<span>{bill.vendor.name}</span>
|
||||||
<span>{bill.date}</span>
|
<span>{bill.date}</span>
|
||||||
@@ -147,7 +146,7 @@ export default function GlobalSearch() {
|
|||||||
}`,
|
}`,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
|
<Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
|
||||||
<Space wrap split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
|
<span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
|
||||||
pb.company || ""
|
pb.company || ""
|
||||||
}`}</span>
|
}`}</span>
|
||||||
@@ -166,10 +165,10 @@ export default function GlobalSearch() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
dropdownMatchSelectWidth={"false"}
|
|
||||||
options={options}
|
options={options}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
allowClear
|
allowClear
|
||||||
|
placeholder={t("general.labels.globalsearch")}
|
||||||
>
|
>
|
||||||
<Input.Search loading={loading} />
|
<Input.Search loading={loading} />
|
||||||
</AutoComplete>
|
</AutoComplete>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import Icon, {
|
|||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
CarFilled,
|
CarFilled,
|
||||||
ClockCircleFilled,
|
ClockCircleFilled,
|
||||||
|
DashboardFilled,
|
||||||
DollarCircleFilled,
|
DollarCircleFilled,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
FieldTimeOutlined,
|
FieldTimeOutlined,
|
||||||
FileAddFilled,
|
FileAddFilled,
|
||||||
|
FileAddOutlined,
|
||||||
FileFilled,
|
FileFilled,
|
||||||
GlobalOutlined,
|
GlobalOutlined,
|
||||||
HomeFilled,
|
HomeFilled,
|
||||||
@@ -14,6 +16,7 @@ import Icon, {
|
|||||||
LineChartOutlined,
|
LineChartOutlined,
|
||||||
PaperClipOutlined,
|
PaperClipOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
|
QuestionCircleFilled,
|
||||||
ScheduleOutlined,
|
ScheduleOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
@@ -44,7 +47,6 @@ import {
|
|||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { signOutStart } from "../../redux/user/user.actions";
|
import { signOutStart } from "../../redux/user/user.actions";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -78,12 +80,11 @@ function Header({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header style={{ display: "flex", alignItems: "center" }}>
|
<Layout.Header>
|
||||||
<Menu
|
<Menu
|
||||||
mode="horizontal"
|
mode="horizontal"
|
||||||
//theme="light"
|
//theme="light"
|
||||||
theme={"dark"}
|
theme={"dark"}
|
||||||
style={{ flex: 1 }}
|
|
||||||
selectedKeys={[selectedHeader]}
|
selectedKeys={[selectedHeader]}
|
||||||
onClick={handleMenuClick}
|
onClick={handleMenuClick}
|
||||||
subMenuCloseDelay={0.3}
|
subMenuCloseDelay={0.3}
|
||||||
@@ -95,6 +96,7 @@ function Header({
|
|||||||
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
<Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="jobssubmenu"
|
||||||
icon={<Icon component={FaCarCrash} />}
|
icon={<Icon component={FaCarCrash} />}
|
||||||
title={t("menus.header.jobs")}
|
title={t("menus.header.jobs")}
|
||||||
>
|
>
|
||||||
@@ -109,12 +111,14 @@ function Header({
|
|||||||
{t("menus.header.availablejobs")}
|
{t("menus.header.availablejobs")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Item key="newjob" icon={<FileAddOutlined />}>
|
||||||
|
<Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Divider key="div1" />
|
||||||
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
|
<Menu.Item key="alljobs" icon={<UnorderedListOutlined />}>
|
||||||
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
<Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider key="div2" />
|
||||||
|
|
||||||
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
|
<Menu.Item key="productionlist" icon={<ScheduleOutlined />}>
|
||||||
<Link to="/manage/production/list">
|
<Link to="/manage/production/list">
|
||||||
{t("menus.header.productionlist")}
|
{t("menus.header.productionlist")}
|
||||||
@@ -125,13 +129,13 @@ function Header({
|
|||||||
{t("menus.header.productionboard")}
|
{t("menus.header.productionboard")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider key="div3" />
|
||||||
|
|
||||||
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
|
<Menu.Item key="scoreboard" icon={<LineChartOutlined />}>
|
||||||
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
<Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="customers"
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
title={t("menus.header.customers")}
|
title={t("menus.header.customers")}
|
||||||
>
|
>
|
||||||
@@ -143,6 +147,7 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="ccs"
|
||||||
icon={<CarFilled />}
|
icon={<CarFilled />}
|
||||||
title={t("menus.header.courtesycars")}
|
title={t("menus.header.courtesycars")}
|
||||||
>
|
>
|
||||||
@@ -163,6 +168,7 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="accounting"
|
||||||
icon={<DollarCircleFilled />}
|
icon={<DollarCircleFilled />}
|
||||||
title={t("menus.header.accounting")}
|
title={t("menus.header.accounting")}
|
||||||
>
|
>
|
||||||
@@ -184,7 +190,7 @@ function Header({
|
|||||||
>
|
>
|
||||||
{t("menus.header.enterbills")}
|
{t("menus.header.enterbills")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider key="div4" />
|
||||||
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -196,11 +202,11 @@ function Header({
|
|||||||
context: null,
|
context: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
icon={<Icon component={FaCreditCard} />}
|
||||||
>
|
>
|
||||||
<Icon component={FaCreditCard} />
|
|
||||||
{t("menus.header.enterpayment")}
|
{t("menus.header.enterpayment")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider key="div5" />
|
||||||
|
|
||||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||||
<Link to="/manage/timetickets">
|
<Link to="/manage/timetickets">
|
||||||
@@ -219,9 +225,10 @@ function Header({
|
|||||||
>
|
>
|
||||||
{t("menus.header.entertimeticket")}
|
{t("menus.header.entertimeticket")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider key="div6" />
|
||||||
|
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="accountingexport"
|
||||||
title={t("menus.header.export")}
|
title={t("menus.header.export")}
|
||||||
icon={<ExportOutlined />}
|
icon={<ExportOutlined />}
|
||||||
>
|
>
|
||||||
@@ -255,11 +262,17 @@ function Header({
|
|||||||
{t("menus.header.temporarydocs")}
|
{t("menus.header.temporarydocs")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.SubMenu title={t("menus.header.shop")} icon={<SettingOutlined />}>
|
<Menu.SubMenu
|
||||||
|
key="shopsubmenu"
|
||||||
|
title={t("menus.header.shop")}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
>
|
||||||
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
|
<Menu.Item key="shop" icon={<Icon component={GiSettingsKnobs} />}>
|
||||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="dashboard" icon={<DashboardFilled />}>
|
||||||
|
<Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="reportcenter"
|
key="reportcenter"
|
||||||
icon={<BarChartOutlined />}
|
icon={<BarChartOutlined />}
|
||||||
@@ -285,17 +298,27 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
style={{ float: "right" }}
|
key="user"
|
||||||
title={
|
title={
|
||||||
currentUser.displayName ||
|
currentUser.displayName ||
|
||||||
currentUser.email ||
|
currentUser.email ||
|
||||||
t("general.labels.unknown")
|
t("general.labels.unknown")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Menu.Item danger onClick={() => signOutStart()}>
|
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
|
||||||
{t("user.actions.signout")}
|
{t("user.actions.signout")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<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={() => {
|
onClick={() => {
|
||||||
window.open("https://imexrescue.com/", "_blank");
|
window.open("https://imexrescue.com/", "_blank");
|
||||||
}}
|
}}
|
||||||
@@ -309,6 +332,7 @@ function Header({
|
|||||||
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
<Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="langselecter"
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<GlobalOutlined />
|
<GlobalOutlined />
|
||||||
@@ -327,7 +351,7 @@ function Header({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.SubMenu style={{ float: "right" }} title={<ClockCircleFilled />}>
|
<Menu.SubMenu key="recent" title={<ClockCircleFilled />}>
|
||||||
{recentItems.map((i, idx) => (
|
{recentItems.map((i, idx) => (
|
||||||
<Menu.Item key={idx}>
|
<Menu.Item key={idx}>
|
||||||
<Link to={i.url}>{i.label}</Link>
|
<Link to={i.url}>{i.label}</Link>
|
||||||
@@ -335,9 +359,6 @@ function Header({
|
|||||||
))}
|
))}
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
</Menu>
|
</Menu>
|
||||||
<div>
|
|
||||||
<GlobalSearch />
|
|
||||||
</div>
|
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
};
|
|
||||||
@@ -87,9 +87,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button type="primary" onClick={showModal}>
|
<Button onClick={showModal}>{t("printcenter.jobs.3rdpartypayer")}</Button>
|
||||||
{t("printcenter.jobs.3rdpartypayer")}
|
|
||||||
</Button>
|
|
||||||
<Modal visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
<Modal visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Button, Popover, Space } from "antd";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
@@ -11,6 +11,8 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
|||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
import ScheduleAtChange from "./job-at-change.component";
|
import ScheduleAtChange from "./job-at-change.component";
|
||||||
import ScheduleEventColor from "./schedule-event.color.component";
|
import ScheduleEventColor from "./schedule-event.color.component";
|
||||||
|
import queryString from "query-string";
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) =>
|
setScheduleContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
@@ -24,6 +26,8 @@ export function ScheduleEventComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const history = useHistory();
|
||||||
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
const blockContent = (
|
const blockContent = (
|
||||||
<div>
|
<div>
|
||||||
@@ -88,6 +92,20 @@ export function ScheduleEventComponent({
|
|||||||
<Button>{t("appointments.actions.viewjob")}</Button>
|
<Button>{t("appointments.actions.viewjob")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
|
{event.job ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
history.push({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...searchParams,
|
||||||
|
selected: event.job.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("appointments.actions.preview")}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const Template = TemplateList("job").appointment_reminder;
|
const Template = TemplateList("job").appointment_reminder;
|
||||||
@@ -97,7 +115,8 @@ export function ScheduleEventComponent({
|
|||||||
variables: { id: event.job.id },
|
variables: { id: event.job.id },
|
||||||
},
|
},
|
||||||
{ to: event.job && event.job.ownr_ea, subject: Template.subject },
|
{ to: event.job && event.job.ownr_ea, subject: Template.subject },
|
||||||
"e"
|
"e",
|
||||||
|
event.job && event.job.id
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
disabled={event.arrived}
|
disabled={event.arrived}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "../../../../redux/user/user.selectors";
|
} from "../../../../redux/user/user.selectors";
|
||||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||||
|
import moment from "moment-business-days";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -81,6 +82,7 @@ export function JobChecklistForm({
|
|||||||
|
|
||||||
...(type === "deliver" && {
|
...(type === "deliver" && {
|
||||||
scheduled_delivery: values.scheduled_delivery,
|
scheduled_delivery: values.scheduled_delivery,
|
||||||
|
actual_delivery: values.actual_delivery,
|
||||||
}),
|
}),
|
||||||
...(type === "deliver" &&
|
...(type === "deliver" &&
|
||||||
values.removeFromProduction && {
|
values.removeFromProduction && {
|
||||||
@@ -133,12 +135,20 @@ export function JobChecklistForm({
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
addToProduction: true,
|
addToProduction: true,
|
||||||
scheduled_completion: job && job.scheduled_completion,
|
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_delivery: job && job.scheduled_delivery,
|
scheduled_delivery: job && job.scheduled_delivery,
|
||||||
}),
|
}),
|
||||||
...(type === "deliver" && {
|
...(type === "deliver" && {
|
||||||
removeFromProduction: true,
|
removeFromProduction: true,
|
||||||
actual_completion: job && job.actual_completion,
|
actual_completion: job && job.actual_completion,
|
||||||
|
actual_delivery: job && job.actual_delivery,
|
||||||
}),
|
}),
|
||||||
...formItems
|
...formItems
|
||||||
.filter((fi) => fi.value)
|
.filter((fi) => fi.value)
|
||||||
@@ -171,21 +181,21 @@ export function JobChecklistForm({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_delivery"
|
name="scheduled_delivery"
|
||||||
label={t("jobs.fields.scheduled_delivery")}
|
label={t("jobs.fields.scheduled_delivery")}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
>
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["production_vars", "note"]}
|
name={["production_vars", "note"]}
|
||||||
label={t("jobs.fields.production_vars.note")}
|
label={t("jobs.fields.production_vars.note")}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={3} />
|
<Input.TextArea rows={3} disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -202,7 +212,14 @@ export function JobChecklistForm({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<DateTimePicker />
|
<DateTimePicker disabled={readOnly} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="actual_delivery"
|
||||||
|
label={t("jobs.fields.actual_delivery")}
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<DateTimePicker disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="removeFromProduction"
|
name="removeFromProduction"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
import ConfigFormComponents from "../config-form-components/config-form-components.component";
|
||||||
|
|
||||||
export default function JobChecklistDisplay({ checklist }) {
|
export default function JobChecklistDisplay({ checklist }) {
|
||||||
console.log("JobChecklistDisplay -> checklist", checklist);
|
|
||||||
if (!checklist) return <div></div>;
|
if (!checklist) return <div></div>;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function JobDetailCardsNotesComponent({ loading, data }) {
|
|||||||
bordered
|
bordered
|
||||||
dataSource={data.notes}
|
dataSource={data.notes}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<List.Item>
|
<List.Item style={{ whiteSpace: "pre-line" }}>
|
||||||
{item.critical ? (
|
{item.critical ? (
|
||||||
<EyeInvisibleFilled style={{ margin: 4, color: "red" }} />
|
<EyeInvisibleFilled style={{ margin: 4, color: "red" }} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
FilterFilled,
|
FilterFilled,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
WarningFilled,
|
WarningFilled,
|
||||||
|
EditFilled,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {
|
import {
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -22,6 +24,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries";
|
import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||||
import { onlyUnique } from "../../utils/arrayHelper";
|
import { onlyUnique } from "../../utils/arrayHelper";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
@@ -37,6 +40,7 @@ import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.con
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
|
technician: selectTechnician,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -48,6 +52,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function JobLinesComponent({
|
export function JobLinesComponent({
|
||||||
jobRO,
|
jobRO,
|
||||||
|
technician,
|
||||||
setPartsOrderContext,
|
setPartsOrderContext,
|
||||||
loading,
|
loading,
|
||||||
refetch,
|
refetch,
|
||||||
@@ -155,7 +160,16 @@ export function JobLinesComponent({
|
|||||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
<>
|
||||||
|
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||||
|
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||||
|
<span
|
||||||
|
style={{ marginLeft: ".2rem" }}
|
||||||
|
>{`(${record.prt_dsmk_p}%)`}</span>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -274,12 +288,12 @@ export function JobLinesComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("general.actions.edit")}
|
<EditFilled />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
onClick={() =>
|
onClick={async () => {
|
||||||
deleteJobLine({
|
await deleteJobLine({
|
||||||
variables: { joblineId: record.id },
|
variables: { joblineId: record.id },
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
@@ -293,8 +307,12 @@ export function JobLinesComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
await axios.post("/job/totalsssu", {
|
||||||
|
id: job.id,
|
||||||
|
});
|
||||||
|
refetch && refetch();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DeleteFilled />
|
<DeleteFilled />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -313,9 +331,12 @@ export function JobLinesComponent({
|
|||||||
if (e.key === "clear") {
|
if (e.key === "clear") {
|
||||||
setSelectedLines([]);
|
setSelectedLines([]);
|
||||||
} else {
|
} else {
|
||||||
|
const markedTypes = [e.key];
|
||||||
|
if (e.key === "PAN") markedTypes.push("PAP");
|
||||||
|
if (e.key === "PAS") markedTypes.push("PASL");
|
||||||
setSelectedLines([
|
setSelectedLines([
|
||||||
...selectedLines,
|
...selectedLines,
|
||||||
...jobLines.filter((item) => item.part_type === e.key),
|
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -354,7 +375,8 @@ export function JobLinesComponent({
|
|||||||
disabled={
|
disabled={
|
||||||
(job && !job.converted) ||
|
(job && !job.converted) ||
|
||||||
(selectedLines.length > 0 ? false : true) ||
|
(selectedLines.length > 0 ? false : true) ||
|
||||||
jobRO
|
jobRO ||
|
||||||
|
technician
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPartsOrderContext({
|
setPartsOrderContext({
|
||||||
@@ -378,7 +400,7 @@ export function JobLinesComponent({
|
|||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
filteredInfo: {
|
filteredInfo: {
|
||||||
part_type: ["PAN,PAL,PAA,PAS,PASL"],
|
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -389,7 +411,7 @@ export function JobLinesComponent({
|
|||||||
<Button>{t("jobs.actions.mark")}</Button>
|
<Button>{t("jobs.actions.mark")}</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO || technician}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setJobLineEditContext({
|
setJobLineEditContext({
|
||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { DownOutlined } from "@ant-design/icons";
|
||||||
|
import { Dropdown, Menu } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JoblinePresetButton({ bodyshop, form }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleSelect = (item) => {
|
||||||
|
form.setFieldsValue(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Menu>
|
||||||
|
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||||
|
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
|
||||||
|
{i.label}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Dropdown trigger={["click"]} overlay={menu}>
|
||||||
|
<a
|
||||||
|
className="ant-dropdown-link"
|
||||||
|
href="# "
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("joblines.labels.presets")} <DownOutlined />
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JoblinePresetButton);
|
||||||
@@ -3,7 +3,7 @@ import React, { useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import JoblinesPreset from "../job-lines-preset-button/job-lines-preset-button.component";
|
||||||
export default function JobLinesUpsertModalComponent({
|
export default function JobLinesUpsertModalComponent({
|
||||||
visible,
|
visible,
|
||||||
jobLine,
|
jobLine,
|
||||||
@@ -32,6 +32,7 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: loading }}
|
okButtonProps={{ loading: loading }}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
|
e
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
@@ -41,6 +42,9 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
form={form}
|
form={form}
|
||||||
>
|
>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
|
<Form.Item label={t("joblines.fields.line_no")} name="line_no">
|
||||||
|
<InputNumber />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.line_desc")}
|
label={t("joblines.fields.line_desc")}
|
||||||
rules={[
|
rules={[
|
||||||
@@ -53,6 +57,7 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<JoblinesPreset form={form} />
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
<Form.Item label={t("joblines.fields.mod_lbr_ty")} name="mod_lbr_ty">
|
||||||
@@ -123,7 +128,7 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
// }),
|
// }),
|
||||||
// ]}
|
// ]}
|
||||||
>
|
>
|
||||||
<InputCurrency />
|
<InputNumber precision={1} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
@@ -210,6 +215,13 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
>
|
>
|
||||||
<InputCurrency precision={2} min={0} />
|
<InputCurrency precision={2} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("joblines.fields.prt_dsmk_p")}
|
||||||
|
name="prt_dsmk_p"
|
||||||
|
initialValue={0}
|
||||||
|
>
|
||||||
|
<InputNumber precision={0} min={0} max={100} />
|
||||||
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
|||||||
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
||||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||||
|
import Axios from "axios";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobLineEditModal: selectJobLineEditModal,
|
jobLineEditModal: selectJobLineEditModal,
|
||||||
});
|
});
|
||||||
@@ -29,10 +29,10 @@ function JobLinesUpsertModalContainer({
|
|||||||
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (!jobLineEditModal.context.id) {
|
if (!jobLineEditModal.context.id) {
|
||||||
insertJobLine({
|
const r = await insertJobLine({
|
||||||
variables: {
|
variables: {
|
||||||
lineInput: [
|
lineInput: [
|
||||||
{
|
{
|
||||||
@@ -44,42 +44,44 @@ function JobLinesUpsertModalContainer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.then((r) => {
|
if (!r.errors) {
|
||||||
if (jobLineEditModal.actions.refetch)
|
await Axios.post("/job/totalsssu", {
|
||||||
jobLineEditModal.actions.refetch();
|
id: jobLineEditModal.context.jobid,
|
||||||
//Need to recalcuate totals.
|
|
||||||
toggleModalVisible();
|
|
||||||
notification["success"]({
|
|
||||||
message: t("joblines.successes.created"),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("joblines.errors.creating", {
|
|
||||||
message: error.message,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
if (jobLineEditModal.actions.refetch)
|
||||||
|
jobLineEditModal.actions.refetch();
|
||||||
|
//Need to recalcuate totals.
|
||||||
|
toggleModalVisible();
|
||||||
|
notification["success"]({
|
||||||
|
message: t("joblines.successes.created"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("joblines.errors.creating", {
|
||||||
|
message: JSON.stringify(r.errors.message),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
updateJobLine({
|
const r = await updateJobLine({
|
||||||
variables: {
|
variables: {
|
||||||
lineId: jobLineEditModal.context.id,
|
lineId: jobLineEditModal.context.id,
|
||||||
line: values,
|
line: values,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.then((r) => {
|
if (!r.errors) {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("joblines.successes.updated"),
|
message: t("joblines.successes.updated"),
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
notification["success"]({
|
|
||||||
message: t("joblines.errors.updating", {
|
|
||||||
message: error.message,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("joblines.errors.updating", {
|
||||||
|
message: JSON.stringify(r.errors.message),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (jobLineEditModal.actions.submit) {
|
if (jobLineEditModal.actions.submit) {
|
||||||
jobLineEditModal.actions.submit();
|
jobLineEditModal.actions.submit();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
|||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
@@ -40,14 +40,14 @@ export function JobPayments({
|
|||||||
});
|
});
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("payments.fields.created_at"),
|
title: t("payments.fields.date"),
|
||||||
dataIndex: "created_at",
|
dataIndex: "date",
|
||||||
key: "created_at",
|
key: "date",
|
||||||
|
sorter: (a, b) => dateSort(a.date, b.date),
|
||||||
|
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>,
|
||||||
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.payer"),
|
title: t("payments.fields.payer"),
|
||||||
@@ -123,6 +123,7 @@ export function JobPayments({
|
|||||||
messageObject={{
|
messageObject={{
|
||||||
to: job.ownr_ea,
|
to: job.ownr_ea,
|
||||||
}}
|
}}
|
||||||
|
id={job.id}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -154,7 +155,7 @@ export function JobPayments({
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={!job.converted}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPaymentContext({
|
setPaymentContext({
|
||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const JobSearchSelect = (
|
|||||||
{theOptions
|
{theOptions
|
||||||
? theOptions.map((o) => (
|
? theOptions.map((o) => (
|
||||||
<Option key={o.id} value={o.id} status={o.status}>
|
<Option key={o.id} value={o.id} status={o.status}>
|
||||||
{`${clm_no ? `${o.clm_no} | ` : ""}${
|
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
|
||||||
o.ro_number || t("general.labels.na")
|
o.ro_number || t("general.labels.na")
|
||||||
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
|
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
|
||||||
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
|
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
|
||||||
|
|||||||
@@ -69,17 +69,28 @@ export default function JobTotalsTableParts({ job }) {
|
|||||||
x: true,
|
x: true,
|
||||||
}}
|
}}
|
||||||
summary={() => (
|
summary={() => (
|
||||||
<Table.Summary.Row>
|
<>
|
||||||
<Table.Summary.Cell>
|
<Table.Summary.Row>
|
||||||
<strong>{t("jobs.labels.partstotal")}</strong>
|
<Table.Summary.Cell>
|
||||||
</Table.Summary.Cell>
|
{t("jobs.labels.prt_dsmk_total")}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
<Table.Summary.Cell align="right">
|
<Table.Summary.Cell align="right">
|
||||||
<strong>
|
{Dinero(job.job_totals.parts.parts.prt_dsmk_total).toFormat()}
|
||||||
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
</Table.Summary.Cell>
|
||||||
</strong>
|
</Table.Summary.Row>
|
||||||
</Table.Summary.Cell>
|
<Table.Summary.Row>
|
||||||
</Table.Summary.Row>
|
<Table.Summary.Cell>
|
||||||
|
<strong>{t("jobs.labels.partstotal")}</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
|
||||||
|
<Table.Summary.Cell align="right">
|
||||||
|
<strong>
|
||||||
|
{Dinero(job.job_totals.parts.parts.total).toFormat()}
|
||||||
|
</strong>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ mutation UNVOID_JOB($jobId: uuid!) {
|
|||||||
}
|
}
|
||||||
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
|
insert_notes(objects: {jobid: $jobId, audit: true, created_by: "${
|
||||||
currentUser.email
|
currentUser.email
|
||||||
}", text: "${t("jobs.labels.unvoidnote", { email: currentUser.email })}"}) {
|
}", text: "${t("jobs.labels.unvoidnote")}"}) {
|
||||||
returning {
|
returning {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
).data;
|
).data;
|
||||||
|
|
||||||
let existingVehicles;
|
let existingVehicles;
|
||||||
if (estData.data.available_jobs_by_pk.est_data.vehicle) {
|
if (
|
||||||
|
estData.data.available_jobs_by_pk.est_data.vehicle &&
|
||||||
|
estData.data.available_jobs_by_pk.est_data.vin
|
||||||
|
) {
|
||||||
//There's vehicle data, need to double check the VIN.
|
//There's vehicle data, need to double check the VIN.
|
||||||
existingVehicles = await client.query({
|
existingVehicles = await client.query({
|
||||||
query: SEARCH_VEHICLE_BY_VIN,
|
query: SEARCH_VEHICLE_BY_VIN,
|
||||||
@@ -124,10 +127,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
data: {
|
data: {
|
||||||
created_by: currentUser.email,
|
created_by: currentUser.email,
|
||||||
audit: true,
|
audit: true,
|
||||||
text: t("jobs.labels.importnote", {
|
text: t("jobs.labels.importnote"),
|
||||||
date: moment().format("MM/DD/yyy"),
|
|
||||||
time: moment().format("hh:mm a"),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
queued_for_parts: true,
|
queued_for_parts: true,
|
||||||
@@ -278,10 +278,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
jobid: selectedJob,
|
jobid: selectedJob,
|
||||||
created_by: currentUser.email,
|
created_by: currentUser.email,
|
||||||
audit: true,
|
audit: true,
|
||||||
text: t("jobs.labels.supplementnote", {
|
text: t("jobs.labels.supplementnote"),
|
||||||
date: moment().format("MM/DD/yyy"),
|
|
||||||
time: moment().format("hh:mm a"),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Collapse, Form, Input, Select, Switch } from "antd";
|
import { Collapse, Form, Input, InputNumber, Select, Switch } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -26,12 +26,6 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
const { getFieldValue } = form;
|
const { getFieldValue } = form;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<JobsDetailRatesParts
|
|
||||||
jobRO={false}
|
|
||||||
expanded
|
|
||||||
required={selected && true}
|
|
||||||
form={form}
|
|
||||||
/>
|
|
||||||
<Collapse defaultActiveKey="insurance">
|
<Collapse defaultActiveKey="insurance">
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
key="insurance"
|
key="insurance"
|
||||||
@@ -57,7 +51,13 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
<FormDatePicker />
|
<FormDatePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||||
<Input />
|
<Select>
|
||||||
|
{bodyshop.md_ins_cos.map((s) => (
|
||||||
|
<Select.Option key={s.name} value={s.name}>
|
||||||
|
{s.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||||
<Input />
|
<Input />
|
||||||
@@ -240,6 +240,26 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
<CurrencyInput />
|
<CurrencyInput />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
<LayoutFormRow>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.federal_tax_rate")}
|
||||||
|
name="federal_tax_rate"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={1} precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.state_tax_rate")}
|
||||||
|
name="state_tax_rate"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={1} precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.local_tax_rate")}
|
||||||
|
name="local_tax_rate"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={1} precision={2} />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
<Form.Item label={t("jobs.fields.rate_lab")} name="rate_lab">
|
||||||
<CurrencyInput />
|
<CurrencyInput />
|
||||||
@@ -312,6 +332,12 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
<JobsDetailRatesParts
|
||||||
|
jobRO={false}
|
||||||
|
expanded
|
||||||
|
required={selected && true}
|
||||||
|
form={form}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
import React, { useContext } from "react";
|
import React, { useContext } from "react";
|
||||||
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
import { SEARCH_VEHICLES } from "../../graphql/vehicles.queries";
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
|
import JobsCreateVehicleInfoComponent from "./jobs-create-vehicle-info.component";
|
||||||
import { useQuery } from "@apollo/client";
|
|
||||||
|
|
||||||
export default function JobsCreateVehicleInfoContainer({ form }) {
|
export default function JobsCreateVehicleInfoContainer({ form }) {
|
||||||
const [state] = useContext(JobCreateContext);
|
const [state] = useContext(JobCreateContext);
|
||||||
const { loading, error, data } = useQuery(SEARCH_VEHICLE_BY_VIN, {
|
const { loading, error, data } = useQuery(SEARCH_VEHICLES, {
|
||||||
variables: { vin: `%${state.vehicle.search}%` },
|
variables: { search: `%${state.vehicle.search}%` },
|
||||||
skip: !state.vehicle.search,
|
skip: !state.vehicle.search,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export default function JobsCreateVehicleInfoContainer({ form }) {
|
|||||||
return (
|
return (
|
||||||
<JobsCreateVehicleInfoComponent
|
<JobsCreateVehicleInfoComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
vehicles={data ? data.vehicles : null}
|
vehicles={data ? data.search_vehicles : null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,24 +83,12 @@ export default function JobsCreateVehicleInfoNewComponent() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("vehicles.fields.plate_st")}
|
label={t("vehicles.fields.plate_st")}
|
||||||
name={["vehicle", "data", "plate_st"]}
|
name={["vehicle", "data", "plate_st"]}
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: state.vehicle.new,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input disabled={!state.vehicle.new} />
|
<Input disabled={!state.vehicle.new} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("vehicles.fields.plate_no")}
|
label={t("vehicles.fields.plate_no")}
|
||||||
name={["vehicle", "data", "plate_no"]}
|
name={["vehicle", "data", "plate_no"]}
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: state.vehicle.new,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Input disabled={!state.vehicle.new} />
|
<Input disabled={!state.vehicle.new} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatePicker, Form, Tooltip } from "antd";
|
import { DatePicker, Form, Statistic, Tooltip } from "antd";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -23,6 +23,10 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
);
|
);
|
||||||
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
}, [job.status, bodyshop.md_ro_statuses.post_production_statuses]);
|
||||||
|
|
||||||
|
const calcRepairDays =
|
||||||
|
job.joblines.reduce((acc, val) => acc + val.mod_lb_hrs, 0) /
|
||||||
|
(bodyshop.target_touchtime === 0 ? 1 : bodyshop.target_touchtime);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormRow header={t("jobs.forms.estdates")}>
|
<FormRow header={t("jobs.forms.estdates")}>
|
||||||
@@ -52,6 +56,17 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||||
<DateTimePicker disabled={jobRO} />
|
<DateTimePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Tooltip
|
||||||
|
title={t("jobs.labels.calc_repair_days_tt", {
|
||||||
|
target_touchtime: bodyshop.target_touchtime,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
value={calcRepairDays}
|
||||||
|
precision={1}
|
||||||
|
title={t("jobs.labels.calc_repair_days")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow header={t("jobs.forms.repairdates")}>
|
<FormRow header={t("jobs.forms.repairdates")}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -54,7 +54,13 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
|
||||||
<Input disabled={jobRO} />
|
<Select disabled={jobRO}>
|
||||||
|
{bodyshop.md_ins_cos.map((s) => (
|
||||||
|
<Select.Option key={s.name} value={s.name}>
|
||||||
|
{s.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
<Form.Item label={t("jobs.fields.ins_addr1")} name="ins_addr1">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { DownCircleFilled } from "@ant-design/icons";
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd";
|
import { Button, Dropdown, Menu, notification, Popconfirm } from "antd";
|
||||||
import moment from "moment";
|
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -18,6 +17,7 @@ import {
|
|||||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||||
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
|
import JobsDetaiLheaderCsi from "./jobs-detail-header-actions.csi.component";
|
||||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||||
|
import JobsDetailHeaderActionsExportcustdataComponent from "./jobs-detail-header-actions.exportcustdata.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -156,7 +156,7 @@ export function JobsDetailHeaderActions({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="enterpayments"
|
key="enterpayments"
|
||||||
disabled={jobRO || !job.converted}
|
disabled={!job.converted}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logImEXEvent("job_header_enter_payment");
|
logImEXEvent("job_header_enter_payment");
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ export function JobsDetailHeaderActions({
|
|||||||
{job.inproduction ? (
|
{job.inproduction ? (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="addtoproduction"
|
key="addtoproduction"
|
||||||
disabled={!!!job.converted || jobRO}
|
disabled={!job.converted}
|
||||||
onClick={() => AddToProduction(client, job.id, refetch, true)}
|
onClick={() => AddToProduction(client, job.id, refetch, true)}
|
||||||
>
|
>
|
||||||
{t("jobs.actions.removefromproduction")}
|
{t("jobs.actions.removefromproduction")}
|
||||||
@@ -189,7 +189,7 @@ export function JobsDetailHeaderActions({
|
|||||||
) : (
|
) : (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="addtoproduction"
|
key="addtoproduction"
|
||||||
disabled={!!!job.converted || !!job.inproduction || jobRO}
|
disabled={!job.converted}
|
||||||
onClick={() => AddToProduction(client, job.id, refetch)}
|
onClick={() => AddToProduction(client, job.id, refetch)}
|
||||||
>
|
>
|
||||||
{t("jobs.actions.addtoproduction")}
|
{t("jobs.actions.addtoproduction")}
|
||||||
@@ -201,7 +201,7 @@ export function JobsDetailHeaderActions({
|
|||||||
? t("production.labels.alertoff")
|
? t("production.labels.alertoff")
|
||||||
: t("production.labels.alerton")}
|
: t("production.labels.alerton")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.SubMenu title={t("menus.jobsactions.duplicate")}>
|
<Menu.SubMenu key="dupe" title={t("menus.jobsactions.duplicate")}>
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("jobs.labels.duplicateconfirm")}
|
title={t("jobs.labels.duplicateconfirm")}
|
||||||
@@ -317,6 +317,7 @@ export function JobsDetailHeaderActions({
|
|||||||
{t("menus.jobsactions.admin")}
|
{t("menus.jobsactions.admin")}
|
||||||
</Link>
|
</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<JobsDetailHeaderActionsExportcustdataComponent job={job} />
|
||||||
<JobsDetaiLheaderCsi job={job} />
|
<JobsDetaiLheaderCsi job={job} />
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="jobcosting"
|
key="jobcosting"
|
||||||
@@ -385,10 +386,7 @@ export function JobsDetailHeaderActions({
|
|||||||
jobid: job.id,
|
jobid: job.id,
|
||||||
created_by: currentUser.email,
|
created_by: currentUser.email,
|
||||||
audit: true,
|
audit: true,
|
||||||
text: t("jobs.labels.voidnote", {
|
text: t("jobs.labels.voidnote"),
|
||||||
date: moment().format("MM/DD/yyy"),
|
|
||||||
time: moment().format("hh:mm a"),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser'
|
//currentUser: selectCurrentUser'
|
||||||
@@ -97,6 +98,7 @@ export function JobsDetailHeaderCsi({
|
|||||||
}
|
}
|
||||||
if (e.key === "email")
|
if (e.key === "email")
|
||||||
setEmailOptions({
|
setEmailOptions({
|
||||||
|
jobid: job.id,
|
||||||
messageOptions: {
|
messageOptions: {
|
||||||
to: [job.ownr_ea],
|
to: [job.ownr_ea],
|
||||||
replyTo: bodyshop.email,
|
replyTo: bodyshop.email,
|
||||||
@@ -139,6 +141,7 @@ export function JobsDetailHeaderCsi({
|
|||||||
} else {
|
} else {
|
||||||
if (e.key === "email")
|
if (e.key === "email")
|
||||||
setEmailOptions({
|
setEmailOptions({
|
||||||
|
jobid: job.id,
|
||||||
messageOptions: {
|
messageOptions: {
|
||||||
to: [job.ownr_ea],
|
to: [job.ownr_ea],
|
||||||
replyTo: bodyshop.email,
|
replyTo: bodyshop.email,
|
||||||
@@ -177,8 +180,11 @@ export function JobsDetailHeaderCsi({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!HasFeatureAccess({ featureName: "csi", bodyshop })) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
|
key="sendcsi"
|
||||||
title={t("jobs.actions.sendcsi")}
|
title={t("jobs.actions.sendcsi")}
|
||||||
disabled={!job.converted}
|
disabled={!job.converted}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import { INSERT_NEW_JOB, QUERY_JOB_FOR_DUPE } from "../../graphql/jobs.queries";
|
||||||
INSERT_NEW_JOB,
|
|
||||||
QUERY_ALL_JOB_FIELDS,
|
|
||||||
} from "../../graphql/jobs.queries";
|
|
||||||
|
|
||||||
export default async function DuplicateJob(
|
export default async function DuplicateJob(
|
||||||
apolloClient,
|
apolloClient,
|
||||||
@@ -18,33 +15,21 @@ export default async function DuplicateJob(
|
|||||||
const { defaultOpenStatus } = config;
|
const { defaultOpenStatus } = config;
|
||||||
//get a list of all fields on the job
|
//get a list of all fields on the job
|
||||||
const res = await apolloClient.query({
|
const res = await apolloClient.query({
|
||||||
query: QUERY_ALL_JOB_FIELDS,
|
query: QUERY_JOB_FOR_DUPE,
|
||||||
variables: { id: jobId },
|
variables: { id: jobId },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("res", res);
|
console.log("res", res);
|
||||||
const { jobs_by_pk: existingJob } = res.data;
|
const { jobs_by_pk } = res.data;
|
||||||
|
const existingJob = _.cloneDeep(jobs_by_pk);
|
||||||
|
delete existingJob.__typename;
|
||||||
|
delete existingJob.id;
|
||||||
|
delete existingJob.createdat;
|
||||||
|
delete existingJob.updatedat;
|
||||||
|
|
||||||
const newJob = {
|
const newJob = {
|
||||||
date_estimated: new Date(),
|
...existingJob,
|
||||||
shopid: existingJob.shopid,
|
|
||||||
status: defaultOpenStatus,
|
status: defaultOpenStatus,
|
||||||
ownerid: existingJob.ownerid,
|
|
||||||
ownr_fn: existingJob.ownr_fn,
|
|
||||||
ownr_ln: existingJob.ownr_ln,
|
|
||||||
ownr_co_nm: existingJob.ownr_co_nm,
|
|
||||||
ownr_addr1: existingJob.ownr_addr1,
|
|
||||||
ownr_addr2: existingJob.ownr_addr2,
|
|
||||||
ownr_st: existingJob.ownr_st,
|
|
||||||
ownr_zip: existingJob.ownr_zip,
|
|
||||||
ownr_ctry: existingJob.ownr_ctry,
|
|
||||||
|
|
||||||
ownr_ph1: existingJob.ownr_ph1,
|
|
||||||
vehicleid: existingJob.vehicleid,
|
|
||||||
v_vin: existingJob.v_vin,
|
|
||||||
v_make_desc: existingJob.v_make_desc,
|
|
||||||
v_model_desc: existingJob.v_model_desc,
|
|
||||||
v_model_yr: existingJob.v_model_yr,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const _tempLines = _.cloneDeep(existingJob.joblines);
|
const _tempLines = _.cloneDeep(existingJob.joblines);
|
||||||
@@ -54,24 +39,20 @@ export default async function DuplicateJob(
|
|||||||
line.manual_line = true;
|
line.manual_line = true;
|
||||||
});
|
});
|
||||||
newJob.joblines = keepJobLines ? _tempLines : [];
|
newJob.joblines = keepJobLines ? _tempLines : [];
|
||||||
newJob.job_totals = (
|
|
||||||
await Axios.post("/job/totals", {
|
|
||||||
job: newJob,
|
|
||||||
})
|
|
||||||
).data;
|
|
||||||
|
|
||||||
delete newJob.joblines;
|
delete newJob.joblines;
|
||||||
newJob.joblines = keepJobLines ? { data: _tempLines } : null;
|
newJob.joblines = keepJobLines ? { data: _tempLines } : null;
|
||||||
|
|
||||||
apolloClient
|
const res2 = await apolloClient.mutate({
|
||||||
.mutate({
|
mutation: INSERT_NEW_JOB,
|
||||||
mutation: INSERT_NEW_JOB,
|
variables: { job: [newJob] },
|
||||||
variables: { job: [newJob] },
|
});
|
||||||
})
|
await Axios.post("/job/totalsssu", {
|
||||||
.then((res2) => {
|
id: res2.data.insert_jobs.returning[0].id,
|
||||||
if (completionCallback)
|
});
|
||||||
completionCallback(res2.data.insert_jobs.returning[0].id);
|
|
||||||
});
|
if (completionCallback)
|
||||||
|
completionCallback(res2.data.insert_jobs.returning[0].id);
|
||||||
|
|
||||||
//insert the new job. call the callback with the returned ID when done.
|
//insert the new job. call the callback with the returned ID when done.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Menu, notification } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
|
export function JobsDetailHeaderActionexportCustomerData({
|
||||||
|
bodyshop,
|
||||||
|
job,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleExportCustData = async (e) => {
|
||||||
|
logImEXEvent("job_export_cust_data");
|
||||||
|
let QbXmlResponse;
|
||||||
|
try {
|
||||||
|
QbXmlResponse = await axios.post(
|
||||||
|
"/accounting/qbxml/receivables",
|
||||||
|
{ jobIds: [job.id], custDataOnly: true },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log("handle -> XML", QbXmlResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error getting QBXML from Server.", error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting", {
|
||||||
|
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let PartnerResponse;
|
||||||
|
try {
|
||||||
|
PartnerResponse = await axios.post(
|
||||||
|
"http://localhost:1337/qb/",
|
||||||
|
QbXmlResponse.data,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error connecting to quickbooks or partner.", error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting-partner"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Check to see if any of them failed. If they didn't don't execute the update.
|
||||||
|
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
|
||||||
|
if (failedTransactions.length > 0) {
|
||||||
|
//Uh oh. At least one was no good.
|
||||||
|
failedTransactions.forEach((ft) => {
|
||||||
|
//insert failed export log
|
||||||
|
notification.open({
|
||||||
|
// key: "failedexports",
|
||||||
|
type: "error",
|
||||||
|
message: t("jobs.errors.exporting", {
|
||||||
|
error: ft.errorMessage || "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Handle Failures.
|
||||||
|
} else {
|
||||||
|
//Insert success export log.
|
||||||
|
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "jobsuccessexport",
|
||||||
|
message: t("jobs.successes.exported"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item
|
||||||
|
{...props}
|
||||||
|
onClick={handleExportCustData}
|
||||||
|
key="exportcustdata"
|
||||||
|
disabled={!job.converted}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.exportcustdata")}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobsDetailHeaderActionexportCustomerData);
|
||||||
@@ -151,17 +151,21 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<Card
|
<Card
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
title={
|
title={
|
||||||
<Link
|
job.vehicle ? (
|
||||||
to={
|
<Link
|
||||||
disabled
|
to={
|
||||||
? "#"
|
disabled
|
||||||
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
|
? "#"
|
||||||
}
|
: job.vehicle && `/manage/vehicles/${job.vehicle.id}`
|
||||||
>
|
}
|
||||||
{`${job.v_model_yr || ""} ${job.v_color || ""}
|
>
|
||||||
|
{`${job.v_model_yr || ""} ${job.v_color || ""}
|
||||||
${job.v_make_desc || ""}
|
${job.v_make_desc || ""}
|
||||||
${job.v_model_desc || ""}`}
|
${job.v_model_desc || ""}`}
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span></span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -36,8 +36,11 @@ export function JobsDetailRates({ jobRO, form, job }) {
|
|||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
disabled={jobRO}
|
disabled={jobRO}
|
||||||
max={
|
max={
|
||||||
((job.job_totals && job.job_totals.totals.federal_tax.amount) ||
|
Math.round(
|
||||||
0) / 100
|
(job.job_totals &&
|
||||||
|
job.job_totals.totals.federal_tax.amount) ||
|
||||||
|
0
|
||||||
|
) / 100
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||||
|
|
||||||
|
export const GenerateSrcUrl = (value) => {
|
||||||
|
let extension = value.extension;
|
||||||
|
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
|
||||||
|
|
||||||
|
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
|
||||||
|
value.type
|
||||||
|
)}/upload/${value.key}${extension ? `.${extension}` : ""}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GenerateThumbUrl = (value) => {
|
||||||
|
let extension = value.extension;
|
||||||
|
if (extension && extension.toLowerCase().includes("heic")) extension = "jpg";
|
||||||
|
else if (
|
||||||
|
DetermineFileType(value.type) !== "image" ||
|
||||||
|
(value.type && value.type.includes("application"))
|
||||||
|
)
|
||||||
|
extension = "jpg";
|
||||||
|
|
||||||
|
return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType(
|
||||||
|
value.type
|
||||||
|
)}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${
|
||||||
|
value.key
|
||||||
|
}${extension ? `.${extension}` : ""}`;
|
||||||
|
};
|
||||||
@@ -1,30 +1,87 @@
|
|||||||
import { Button } from "antd";
|
import { Button, Space } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import cleanAxios from "../../utils/CleanAxios";
|
||||||
|
import formatBytes from "../../utils/formatbytes";
|
||||||
|
|
||||||
export default function JobsDocumentsDownloadButton({ galleryImages }) {
|
export default function JobsDocumentsDownloadButton({
|
||||||
|
galleryImages,
|
||||||
|
identifier,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [download, setDownload] = useState(null);
|
||||||
const imagesToDownload = [
|
const imagesToDownload = [
|
||||||
...galleryImages.images.filter((image) => image.isSelected),
|
...galleryImages.images.filter((image) => image.isSelected),
|
||||||
...galleryImages.other.filter((image) => image.isSelected),
|
// ...galleryImages.other.filter((image) => image.isSelected),
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
logImEXEvent("jobs_documents_download");
|
logImEXEvent("jobs_documents_download");
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post("/media/download", {
|
.post("/media/download", {
|
||||||
ids: imagesToDownload.map((_) => _.key),
|
ids: imagesToDownload.map((_) => _.key),
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
window.open(r.data);
|
// window.open(r.data);
|
||||||
|
downloadAs(
|
||||||
|
r.data,
|
||||||
|
`${identifier || "documents"}.zip`,
|
||||||
|
(progressEvent) => {
|
||||||
|
setDownload((currentDownloadState) => {
|
||||||
|
return {
|
||||||
|
downloaded: progressEvent.loaded || 0,
|
||||||
|
speed:
|
||||||
|
(progressEvent.loaded || 0) -
|
||||||
|
((currentDownloadState && currentDownloadState.downloaded) ||
|
||||||
|
0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => setDownload(null)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button disabled={imagesToDownload.length < 1} onClick={handleDownload}>
|
<>
|
||||||
{t("documents.actions.download")}
|
<Button
|
||||||
</Button>
|
loading={!!download}
|
||||||
|
disabled={imagesToDownload.length < 1}
|
||||||
|
onClick={handleDownload}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<span>{t("documents.actions.download")}</span>
|
||||||
|
{download && (
|
||||||
|
<span>{`(${formatBytes(download.downloaded)} @ ${formatBytes(
|
||||||
|
download.speed
|
||||||
|
)} / second)`}</span>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadAs = (url, name, onDownloadProgress, onCompleted) => {
|
||||||
|
cleanAxios
|
||||||
|
.get(url, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
},
|
||||||
|
responseType: "blob",
|
||||||
|
onDownloadProgress: onDownloadProgress,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
onCompleted && onCompleted();
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const url = window.URL.createObjectURL(response.data);
|
||||||
|
a.href = url;
|
||||||
|
a.download = name;
|
||||||
|
a.click();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("error", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { FileExcelFilled } from "@ant-design/icons";
|
import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Card, Col, Row, Space } from "antd";
|
import { Card, Col, Row, Space, Button } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Gallery from "react-grid-gallery";
|
import Gallery from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
import DocumentsUploadComponent from "../documents-upload/documents-upload.component";
|
||||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
||||||
|
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||||
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
|
import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component";
|
||||||
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component";
|
||||||
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component";
|
||||||
@@ -17,11 +18,30 @@ function JobsDocumentsComponent({
|
|||||||
billId,
|
billId,
|
||||||
billsCallback,
|
billsCallback,
|
||||||
totalSize,
|
totalSize,
|
||||||
bodyshop,
|
downloadIdentifier,
|
||||||
ignoreSizeLimit,
|
ignoreSizeLimit,
|
||||||
}) {
|
}) {
|
||||||
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
|
const [galleryImages, setgalleryImages] = useState({ images: [], other: [] });
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
|
||||||
|
const onCurrentImageChange = (index) => {
|
||||||
|
setIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// console.log("Added event listening for reteching.");
|
||||||
|
// window.addEventListener("storage", (ev) => {
|
||||||
|
// if (ev.key === "refetch" && ev.newValue === true) {
|
||||||
|
// refetch && refetch();
|
||||||
|
// localStorage.setItem("refetch", false);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// window.removeEventListener("storage");
|
||||||
|
// };
|
||||||
|
// }, [refetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let documents = data.reduce(
|
let documents = data.reduce(
|
||||||
@@ -29,19 +49,14 @@ function JobsDocumentsComponent({
|
|||||||
const fileType = DetermineFileType(value.type);
|
const fileType = DetermineFileType(value.type);
|
||||||
if (value.type.startsWith("image")) {
|
if (value.type.startsWith("image")) {
|
||||||
acc.images.push({
|
acc.images.push({
|
||||||
src: `${
|
src: GenerateSrcUrl(value),
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
thumbnail: GenerateThumbUrl(value),
|
||||||
}/${DetermineFileType(value.type)}/upload/${value.key}`,
|
|
||||||
thumbnail: `${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
|
||||||
}/${DetermineFileType(value.type)}/upload/${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
|
|
||||||
}/${value.key}`,
|
|
||||||
thumbnailHeight: 225,
|
thumbnailHeight: 225,
|
||||||
thumbnailWidth: 225,
|
thumbnailWidth: 225,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
key: value.key,
|
key: value.key,
|
||||||
extension: value.extension,
|
extension: value.extension,
|
||||||
|
|
||||||
id: value.id,
|
id: value.id,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
size: value.size,
|
size: value.size,
|
||||||
@@ -50,24 +65,18 @@ function JobsDocumentsComponent({
|
|||||||
} else {
|
} else {
|
||||||
let thumb;
|
let thumb;
|
||||||
switch (fileType) {
|
switch (fileType) {
|
||||||
case "video":
|
|
||||||
thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/c_fill,f_png,h_250,w_250/${value.key}`;
|
|
||||||
break;
|
|
||||||
case "raw":
|
case "raw":
|
||||||
thumb = `${window.location.origin}/file.png`;
|
thumb = `${window.location.origin}/file.png`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${value.key}`;
|
thumb = GenerateThumbUrl(value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = value.key.split("/").pop();
|
const fileName = value.key.split("/").pop();
|
||||||
acc.other.push({
|
acc.other.push({
|
||||||
src: `${
|
source: GenerateSrcUrl(value),
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
src: "",
|
||||||
}/${fileType}/upload/${fileType === "video" ? "q_auto/" : ""}${
|
|
||||||
value.key
|
|
||||||
}${fileType === "raw" ? `.${value.extension}` : ""}`,
|
|
||||||
thumbnail: thumb,
|
thumbnail: thumb,
|
||||||
tags: [
|
tags: [
|
||||||
{
|
{
|
||||||
@@ -93,6 +102,7 @@ function JobsDocumentsComponent({
|
|||||||
thumbnailHeight: 225,
|
thumbnailHeight: 225,
|
||||||
thumbnailWidth: 225,
|
thumbnailWidth: 225,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
|
||||||
extension: value.extension,
|
extension: value.extension,
|
||||||
key: value.key,
|
key: value.key,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
@@ -113,11 +123,17 @@ function JobsDocumentsComponent({
|
|||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
<Button onClick={() => refetch && refetch()}>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
<JobsDocumentsGallerySelectAllComponent
|
<JobsDocumentsGallerySelectAllComponent
|
||||||
galleryImages={galleryImages}
|
galleryImages={galleryImages}
|
||||||
setGalleryImages={setgalleryImages}
|
setGalleryImages={setgalleryImages}
|
||||||
/>
|
/>
|
||||||
<JobsDocumentsDownloadButton galleryImages={galleryImages} />
|
<JobsDocumentsDownloadButton
|
||||||
|
galleryImages={galleryImages}
|
||||||
|
identifier={downloadIdentifier}
|
||||||
|
/>
|
||||||
<JobsDocumentsDeleteButton
|
<JobsDocumentsDeleteButton
|
||||||
galleryImages={galleryImages}
|
galleryImages={galleryImages}
|
||||||
deletionCallback={billsCallback || refetch}
|
deletionCallback={billsCallback || refetch}
|
||||||
@@ -144,6 +160,26 @@ function JobsDocumentsComponent({
|
|||||||
<Gallery
|
<Gallery
|
||||||
images={galleryImages.images}
|
images={galleryImages.images}
|
||||||
backdropClosesModal={true}
|
backdropClosesModal={true}
|
||||||
|
currentImageWillChange={onCurrentImageChange}
|
||||||
|
customControls={[
|
||||||
|
<Button
|
||||||
|
key="edit-button"
|
||||||
|
style={{
|
||||||
|
float: "right",
|
||||||
|
zIndex: "5",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const newWindow = window.open(
|
||||||
|
`${window.location.protocol}//${window.location.host}/edit?documentId=${galleryImages.images[index].id}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
);
|
||||||
|
if (newWindow) newWindow.opener = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditFilled />
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
onClickImage={(props) => {
|
onClickImage={(props) => {
|
||||||
window.open(
|
window.open(
|
||||||
props.target.src,
|
props.target.src,
|
||||||
@@ -173,11 +209,12 @@ function JobsDocumentsComponent({
|
|||||||
backgroundImage: <FileExcelFilled />,
|
backgroundImage: <FileExcelFilled />,
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
cursor: "pointer",
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
onClickThumbnail={(index) => {
|
onClickThumbnail={(index) => {
|
||||||
window.open(
|
window.open(
|
||||||
galleryImages.other[index].src,
|
galleryImages.other[index].source,
|
||||||
"_blank",
|
"_blank",
|
||||||
"toolbar=0,location=0,menubar=0"
|
"toolbar=0,location=0,menubar=0"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function JobsDocumentsContainer({
|
|||||||
return (
|
return (
|
||||||
<JobDocuments
|
<JobDocuments
|
||||||
data={(data && data.documents) || documentsList || []}
|
data={(data && data.documents) || documentsList || []}
|
||||||
|
downloadIdentifier={data && data.jobs_by_pk.ro_number}
|
||||||
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
totalSize={data && data.documents_aggregate.aggregate.sum.size}
|
||||||
billId={billId}
|
billId={billId}
|
||||||
jobId={jobId}
|
jobId={jobId}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import axios from "axios";
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { DELETE_DOCUMENT } from "../../graphql/documents.queries";
|
import { DELETE_DOCUMENTS } from "../../graphql/documents.queries";
|
||||||
import cleanAxios from "../../utils/CleanAxios";
|
|
||||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
|
||||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||||
|
|
||||||
export default function JobsDocumentsDeleteButton({
|
export default function JobsDocumentsDeleteButton({
|
||||||
@@ -15,73 +13,57 @@ export default function JobsDocumentsDeleteButton({
|
|||||||
deletionCallback,
|
deletionCallback,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [deleteDocument] = useMutation(DELETE_DOCUMENT);
|
const [deleteDocument] = useMutation(DELETE_DOCUMENTS);
|
||||||
const imagesToDelete = [
|
const imagesToDelete = [
|
||||||
...galleryImages.images.filter((image) => image.isSelected),
|
...galleryImages.images.filter((image) => image.isSelected),
|
||||||
...galleryImages.other.filter((image) => image.isSelected),
|
...galleryImages.other.filter((image) => image.isSelected),
|
||||||
];
|
];
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = async () => {
|
||||||
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
|
logImEXEvent("job_documents_delete", { count: imagesToDelete.length });
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
imagesToDelete.forEach((image) => {
|
const res = await axios.post("/media/delete", {
|
||||||
let timestamp = Math.floor(Date.now() / 1000);
|
ids: imagesToDelete,
|
||||||
let public_id = image.key;
|
|
||||||
|
|
||||||
axios
|
|
||||||
.post("/media/sign", {
|
|
||||||
public_id: public_id,
|
|
||||||
timestamp: timestamp,
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
var signature = response.data;
|
|
||||||
var options = {
|
|
||||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
|
||||||
};
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("api_key", process.env.REACT_APP_CLOUDINARY_API_KEY);
|
|
||||||
formData.append("public_id", public_id);
|
|
||||||
formData.append("timestamp", timestamp);
|
|
||||||
formData.append("signature", signature);
|
|
||||||
|
|
||||||
cleanAxios
|
|
||||||
.post(
|
|
||||||
`${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT_API
|
|
||||||
}/${DetermineFileType(image.type)}/destroy`,
|
|
||||||
formData,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
deleteDocument({ variables: { id: image.id } })
|
|
||||||
.then((r) => {
|
|
||||||
notification.open({
|
|
||||||
key: "docdeletedsuccesfully",
|
|
||||||
type: "success",
|
|
||||||
message: t("documents.successes.delete"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deletionCallback) deletionCallback();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("documents.errors.deleting", {
|
|
||||||
message: JSON.stringify(error),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
//Delete it from our database.
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("documents.errors.deleting_cloudinary", {
|
|
||||||
message: JSON.stringify(error),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const successfulDeletes = [];
|
||||||
|
res.data.forEach((resType) => {
|
||||||
|
Object.keys(resType.deleted).forEach((key) => {
|
||||||
|
if (resType.deleted[key] !== "deleted") {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("documents.errors.deleting_cloudinary", {
|
||||||
|
message: JSON.stringify(resType.deleted[key]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
successfulDeletes.push(key.replace(/\.[^/.]+$/, ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const delres = await deleteDocument({
|
||||||
|
variables: {
|
||||||
|
ids: imagesToDelete
|
||||||
|
.filter((i) => successfulDeletes.includes(i.key))
|
||||||
|
.map((i) => i.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (delres.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("documents.errors.deleting", {
|
||||||
|
message: JSON.stringify(delres.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.open({
|
||||||
|
key: "docdeletedsuccesfully",
|
||||||
|
type: "success",
|
||||||
|
message: t("documents.successes.delete"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deletionCallback) deletionCallback();
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Gallery from "react-grid-gallery";
|
import Gallery from "react-grid-gallery";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
|
||||||
|
|
||||||
function JobsDocumentGalleryExternal({
|
function JobsDocumentGalleryExternal({
|
||||||
data,
|
data,
|
||||||
@@ -15,14 +15,8 @@ function JobsDocumentGalleryExternal({
|
|||||||
let documents = data.reduce((acc, value) => {
|
let documents = data.reduce((acc, value) => {
|
||||||
if (value.type.startsWith("image")) {
|
if (value.type.startsWith("image")) {
|
||||||
acc.push({
|
acc.push({
|
||||||
src: `${
|
src: GenerateSrcUrl(value),
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
thumbnail: GenerateThumbUrl(value),
|
||||||
}/${DetermineFileType(value.type)}/upload/${value.key}`,
|
|
||||||
thumbnail: `${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
|
||||||
}/${DetermineFileType(value.type)}/upload/${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
|
|
||||||
}/${value.key}`,
|
|
||||||
thumbnailHeight: 225,
|
thumbnailHeight: 225,
|
||||||
thumbnailWidth: 225,
|
thumbnailWidth: 225,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Link, useHistory, useLocation } from "react-router-dom";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
|
||||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -32,7 +31,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
dataIndex: "ro_number",
|
dataIndex: "ro_number",
|
||||||
key: "ro_number",
|
key: "ro_number",
|
||||||
width: "8%",
|
width: "8%",
|
||||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
sorter: true, //(a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
sortOrder: sortcolumn === "ro_number" && sortorder,
|
sortOrder: sortcolumn === "ro_number" && sortorder,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
@@ -44,15 +43,15 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.owner"),
|
title: t("jobs.fields.owner"),
|
||||||
dataIndex: "owner",
|
dataIndex: "ownr_ln",
|
||||||
key: "owner",
|
key: "ownr_ln",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
// sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
width: "25%",
|
width: "25%",
|
||||||
// sortOrder: sortcolumn === "owner" && sortorder,
|
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.owner ? (
|
return record.ownerid ? (
|
||||||
<Link to={"/manage/owners/" + record.owner.id}>
|
<Link to={"/manage/owners/" + record.ownerid}>
|
||||||
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
|
{`${record.ownr_fn || ""} ${record.ownr_ln || ""} ${
|
||||||
record.ownr_co_nm || ""
|
record.ownr_co_nm || ""
|
||||||
}`}
|
}`}
|
||||||
@@ -80,7 +79,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
key: "status",
|
key: "status",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
sorter: true, // (a, b) => alphaSort(a.status, b.status),
|
||||||
sortOrder: sortcolumn === "status" && sortorder,
|
sortOrder: sortcolumn === "status" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.status || t("general.labels.na");
|
return record.status || t("general.labels.na");
|
||||||
@@ -117,7 +116,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
key: "plate_no",
|
key: "plate_no",
|
||||||
width: "8%",
|
width: "8%",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
sorter: true, //(a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||||
sortOrder: sortcolumn === "plate_no" && sortorder,
|
sortOrder: sortcolumn === "plate_no" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.plate_no ? record.plate_no : "";
|
return record.plate_no ? record.plate_no : "";
|
||||||
@@ -129,7 +128,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
key: "clm_no",
|
key: "clm_no",
|
||||||
width: "12%",
|
width: "12%",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
sorter: true, //(a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.clm_no ? (
|
return record.clm_no ? (
|
||||||
@@ -150,7 +149,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
dataIndex: "clm_total",
|
dataIndex: "clm_total",
|
||||||
key: "clm_total",
|
key: "clm_total",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
sorter: true, //(a, b) => a.clm_total - b.clm_total,
|
||||||
sortOrder: sortcolumn === "clm_total" && sortorder,
|
sortOrder: sortcolumn === "clm_total" && sortorder,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.clm_total ? (
|
return record.clm_total ? (
|
||||||
@@ -173,7 +172,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
|||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
search.page = pagination.current;
|
search.page = pagination.current;
|
||||||
search.sortcolumn = sorter.columnKey;
|
search.sortcolumn = sorter.column && sorter.column.key;
|
||||||
search.sortorder = sorter.order;
|
search.sortorder = sorter.order;
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
search.statusFilters = JSON.stringify(_.flattenDeep(filters.status));
|
search.statusFilters = JSON.stringify(_.flattenDeep(filters.status));
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ export function JobNotesComponent({
|
|||||||
dataIndex: "text",
|
dataIndex: "text",
|
||||||
key: "text",
|
key: "text",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ whiteSpace: "pre-line" }}>{text}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export function LaborAllocationsTable({
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<strong
|
<strong
|
||||||
style={{
|
style={{
|
||||||
color: record.difference > 0 ? "green" : "red",
|
color: record.difference >= 0 ? "green" : "red",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{record.difference}
|
{record.difference}
|
||||||
|
|||||||
@@ -31,55 +31,19 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow header={t("owners.forms.address")}>
|
<LayoutFormRow header={t("owners.forms.address")}>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
|
||||||
label={t("owners.fields.ownr_addr1")}
|
|
||||||
name="ownr_addr1"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.ownr_addr2")} name="ownr_addr2">
|
<Form.Item label={t("owners.fields.ownr_addr2")} name="ownr_addr2">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_city")} name="ownr_city">
|
||||||
label={t("owners.fields.ownr_city")}
|
|
||||||
name="ownr_city"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_st")} name="ownr_st">
|
||||||
label={t("owners.fields.ownr_st")}
|
|
||||||
name="ownr_st"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item label={t("owners.fields.ownr_zip")} name="ownr_zip">
|
||||||
label={t("owners.fields.ownr_zip")}
|
|
||||||
name="ownr_zip"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("owners.fields.ownr_ctry")} name="ownr_ctry">
|
<Form.Item label={t("owners.fields.ownr_ctry")} name="ownr_ctry">
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export function PartsOrderListTableComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
<Button
|
<Button
|
||||||
disabled={jobRO}
|
disabled={jobRO ? !record.return : jobRO}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logImEXEvent("parts_order_receive_bill");
|
logImEXEvent("parts_order_receive_bill");
|
||||||
|
|
||||||
@@ -183,6 +183,7 @@ export function PartsOrderListTableComponent({
|
|||||||
? Templates.parts_return_slip.subject
|
? Templates.parts_return_slip.subject
|
||||||
: Templates.parts_order.subject,
|
: Templates.parts_order.subject,
|
||||||
}}
|
}}
|
||||||
|
id={job.id}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
@@ -237,6 +238,11 @@ export function PartsOrderListTableComponent({
|
|||||||
<DateFormatter>{record.deliver_by}</DateFormatter>
|
<DateFormatter>{record.deliver_by}</DateFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.orderedby"),
|
||||||
|
dataIndex: "orderedby",
|
||||||
|
key: "orderedby",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
@@ -336,6 +342,7 @@ export function PartsOrderListTableComponent({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export default function PartsOrderModalComponent({
|
|||||||
<Form.Item required={false} key={field.key}>
|
<Form.Item required={false} key={field.key}>
|
||||||
<LayoutFormRow grow noDivider>
|
<LayoutFormRow grow noDivider>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
span={8}
|
||||||
label={t("parts_orders.fields.line_desc")}
|
label={t("parts_orders.fields.line_desc")}
|
||||||
key={`${index}line_desc`}
|
key={`${index}line_desc`}
|
||||||
name={[field.name, "line_desc"]}
|
name={[field.name, "line_desc"]}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export function PartsOrderModalContainer({
|
|||||||
po: [
|
po: [
|
||||||
{
|
{
|
||||||
...values,
|
...values,
|
||||||
|
orderedby: currentUser.email,
|
||||||
jobid: jobId,
|
jobid: jobId,
|
||||||
user_email: currentUser.email,
|
user_email: currentUser.email,
|
||||||
return: isReturn,
|
return: isReturn,
|
||||||
@@ -179,7 +180,8 @@ export function PartsOrderModalContainer({
|
|||||||
? Templates.parts_return_slip.subject
|
? Templates.parts_return_slip.subject
|
||||||
: Templates.parts_order.subject,
|
: Templates.parts_order.subject,
|
||||||
},
|
},
|
||||||
"e"
|
"e",
|
||||||
|
jobId
|
||||||
);
|
);
|
||||||
} else if (sendType === "p") {
|
} else if (sendType === "p") {
|
||||||
GenerateDocument(
|
GenerateDocument(
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ export default function PaymentFormTotalPayments({ jobid }) {
|
|||||||
|
|
||||||
if (!data) return <></>;
|
if (!data) return <></>;
|
||||||
const totalPayments = data.jobs_by_pk.payments.reduce((acc, val) => {
|
const totalPayments = data.jobs_by_pk.payments.reduce((acc, val) => {
|
||||||
return acc.add(Dinero({ amount: (val.amount || 0) * 100 }));
|
return acc.add(
|
||||||
|
Dinero({ amount: Math.round(((val && val.amount) || 0) * 100) })
|
||||||
|
);
|
||||||
}, Dinero());
|
}, Dinero());
|
||||||
|
|
||||||
const balance =
|
const balance =
|
||||||
@@ -39,7 +41,7 @@ export default function PaymentFormTotalPayments({ jobid }) {
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t("payments.labels.balance")}
|
title={t("payments.labels.balance")}
|
||||||
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
||||||
value={balance.toFormat()}
|
value={(balance && balance.toFormat()) || ""}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!balance && <div>{t("jobs.errors.nofinancial")}</div>}
|
{!balance && <div>{t("jobs.errors.nofinancial")}</div>}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||||
import { Button, Form, Modal, notification } from "antd";
|
import { Button, Form, Modal, notification } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { GET_JOB_INFO_FOR_STRIPE } from "../../graphql/jobs.queries";
|
||||||
import {
|
import {
|
||||||
INSERT_NEW_PAYMENT,
|
INSERT_NEW_PAYMENT,
|
||||||
UPDATE_PAYMENT,
|
UPDATE_PAYMENT,
|
||||||
@@ -44,6 +45,7 @@ function PaymentModalContainer({
|
|||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||||
const [updatePayment] = useMutation(UPDATE_PAYMENT);
|
const [updatePayment] = useMutation(UPDATE_PAYMENT);
|
||||||
|
const client = useApolloClient();
|
||||||
const stripe = useStripe();
|
const stripe = useStripe();
|
||||||
const elements = useElements();
|
const elements = useElements();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -80,13 +82,22 @@ function PaymentModalContainer({
|
|||||||
stripe_acct_id: bodyshop.stripe_acct_id,
|
stripe_acct_id: bodyshop.stripe_acct_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data } = await client.query({
|
||||||
|
query: GET_JOB_INFO_FOR_STRIPE,
|
||||||
|
variables: { jobid: values.jobid },
|
||||||
|
});
|
||||||
|
|
||||||
stripePayment = await stripe.confirmCardPayment(
|
stripePayment = await stripe.confirmCardPayment(
|
||||||
secretKey.data.clientSecret,
|
secretKey.data.clientSecret,
|
||||||
{
|
{
|
||||||
payment_method: {
|
payment_method: {
|
||||||
card: elements.getElement(CardElement),
|
card: elements.getElement(CardElement),
|
||||||
billing_details: {
|
billing_details: {
|
||||||
name: "Jenny Rosen",
|
name: `${data.jobs_by_pk.ownr_fn || ""} ${
|
||||||
|
data.jobs_by_pk.ownr_ln || ""
|
||||||
|
} ${data.jobs_by_pk.ownr_co_nm || ""}`,
|
||||||
|
email: data.jobs_by_pk.ownr_ea,
|
||||||
|
phone: data.jobs_by_pk.ownr_ph1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -133,7 +144,8 @@ function PaymentModalContainer({
|
|||||||
replyTo: bodyshop.email,
|
replyTo: bodyshop.email,
|
||||||
subject: Templates.payment_receipt.subject,
|
subject: Templates.payment_receipt.subject,
|
||||||
},
|
},
|
||||||
sendby === "email" ? "e" : "p"
|
sendby === "email" ? "e" : "p",
|
||||||
|
paymentObj.jobid
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
@@ -15,6 +15,8 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
|||||||
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
|
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
|
||||||
|
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -128,6 +130,18 @@ export function PaymentsListPaginated({
|
|||||||
title: t("payments.fields.stripeid"),
|
title: t("payments.fields.stripeid"),
|
||||||
dataIndex: "stripeid",
|
dataIndex: "stripeid",
|
||||||
key: "stripeid",
|
key: "stripeid",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.stripeid ? (
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
stripeTestEnv
|
||||||
|
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${record.stripeid}`
|
||||||
|
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${record.stripeid}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{record.stripeid}
|
||||||
|
</a>
|
||||||
|
) : null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("payments.fields.created_at"),
|
title: t("payments.fields.created_at"),
|
||||||
@@ -160,7 +174,7 @@ export function PaymentsListPaginated({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("general.actions.edit")}
|
<EditFilled />
|
||||||
</Button>
|
</Button>
|
||||||
<PrintWrapperComponent
|
<PrintWrapperComponent
|
||||||
templateObject={{
|
templateObject={{
|
||||||
@@ -168,6 +182,7 @@ export function PaymentsListPaginated({
|
|||||||
variables: { id: record.id },
|
variables: { id: record.id },
|
||||||
}}
|
}}
|
||||||
messageObject={{ subject: Templates.payment_receipt.subject }}
|
messageObject={{ subject: Templates.payment_receipt.subject }}
|
||||||
|
id={record.job && record.job.id}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -82,7 +82,31 @@ export function PhonebookFormComponent({
|
|||||||
<Form.Item label={t("phonebook.fields.lastname")} name="lastname">
|
<Form.Item label={t("phonebook.fields.lastname")} name="lastname">
|
||||||
<Input disabled={hasNoAccess} />
|
<Input disabled={hasNoAccess} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("phonebook.fields.company")} name="company">
|
<Form.Item
|
||||||
|
label={t("phonebook.fields.company")}
|
||||||
|
name="company"
|
||||||
|
dependencies={["firstname", "lastname"]}
|
||||||
|
rules={[
|
||||||
|
({ getFieldsValue }) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
const { firstname, lastname, company } = getFieldsValue([
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"company",
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(firstname && firstname.trim() !== "") ||
|
||||||
|
(lastname && lastname.trim() !== "") ||
|
||||||
|
(company && company.trim() !== "")
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(t("phonebook.labels.onenamerequired"));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Input disabled={hasNoAccess} />
|
<Input disabled={hasNoAccess} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|||||||