Compare commits
156 Commits
feature/no
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
706f300750 | ||
|
|
4fad4e41c2 | ||
|
|
1e88d5ae1b | ||
|
|
7ba3cc5ffa | ||
|
|
4fdd48c279 | ||
|
|
f5834ae6bc | ||
|
|
43fbf32e99 | ||
|
|
1b2afb9e93 | ||
|
|
77cb6a4acc | ||
|
|
6a109d63ce | ||
|
|
17c3b24380 | ||
|
|
a6610309e9 | ||
|
|
c63f2ed035 | ||
|
|
db02b9c1c2 | ||
|
|
2c760948c5 | ||
|
|
446bd9035f | ||
|
|
1f16abf303 | ||
|
|
f116e89c94 | ||
|
|
f0ba00aeb8 | ||
|
|
0089e50a29 | ||
|
|
c01f402f92 | ||
|
|
92fb519642 | ||
|
|
fa1dbc2611 | ||
|
|
447298c07d | ||
|
|
4844c42425 | ||
|
|
82db7a1f14 | ||
|
|
fde0681a93 | ||
|
|
b36b4cb213 | ||
|
|
77cbbef085 | ||
|
|
a1472cd9ff | ||
|
|
d32fd9e697 | ||
|
|
fe5e2a247a | ||
|
|
9bf3974ba0 | ||
|
|
42195fccea | ||
|
|
9491d5f069 | ||
|
|
e003768969 | ||
|
|
d6c8d97715 | ||
|
|
ba55717683 | ||
|
|
78dd14af85 | ||
|
|
e109b5102c | ||
|
|
644f269629 | ||
|
|
320aa9c177 | ||
|
|
a0b238c4bb | ||
|
|
fd8dab911f | ||
|
|
3666b7cd22 | ||
|
|
e846d7fce4 | ||
|
|
46b9359dcb | ||
|
|
bd6553f8e4 | ||
|
|
674c06665c | ||
|
|
3feb1a3887 | ||
|
|
7be8322a14 | ||
|
|
534f75c9b1 | ||
|
|
87f68f1840 | ||
|
|
1258843f3d | ||
|
|
485b5d0866 | ||
|
|
62b1da0b64 | ||
|
|
a855853230 | ||
|
|
d28d4d6283 | ||
|
|
912756e0f9 | ||
|
|
908c17aa68 | ||
|
|
96995fdd1b | ||
|
|
5341d93e29 | ||
|
|
b8d2dbc2e1 | ||
|
|
595ec72edd | ||
|
|
885a861f1e | ||
|
|
b213e5d54f | ||
|
|
653692b2a5 | ||
|
|
e1e5dda710 | ||
|
|
7f3b1413d7 | ||
|
|
528c68695f | ||
|
|
65a18acdc1 | ||
|
|
930b2791f2 | ||
|
|
1cf5a1fba8 | ||
|
|
83137b2d96 | ||
|
|
5832a5f529 | ||
|
|
f3ee20e89d | ||
|
|
bcd062dffd | ||
|
|
bd8b961bda | ||
|
|
43b6bdb024 | ||
|
|
3dfc6eede2 | ||
|
|
e70c11d4e6 | ||
|
|
abc7262584 | ||
|
|
e10ca9897c | ||
|
|
7e969e32b2 | ||
|
|
6f561e4caa | ||
|
|
97beb14209 | ||
|
|
5568a434b9 | ||
|
|
e92827aeb2 | ||
|
|
ac890bd92b | ||
|
|
0eaf23841a | ||
|
|
82c13eae9e | ||
|
|
708eb3c73f | ||
|
|
cdb8e48f0d | ||
|
|
90600cdff4 | ||
|
|
cdcfea988f | ||
|
|
26f58961a0 | ||
|
|
8daa0ac154 | ||
|
|
76fee429ea | ||
|
|
d1a65530a3 | ||
|
|
4613a93d09 | ||
|
|
faf1d638fb | ||
|
|
55144bd621 | ||
|
|
bbf908e5e1 | ||
|
|
9da126879e | ||
|
|
18fa00785c | ||
|
|
3192e918a4 | ||
|
|
70e2fd000c | ||
|
|
f9fdd95491 | ||
|
|
45354417d0 | ||
|
|
70749cdef5 | ||
|
|
6c3d29ba91 | ||
|
|
eca5e8241a | ||
|
|
cf23266831 | ||
|
|
09d54722f0 | ||
|
|
d78955a8fd | ||
|
|
467841bea2 | ||
|
|
e56424c9b3 | ||
|
|
a1e4f3827d | ||
|
|
5461aae6f6 | ||
|
|
55fa2a9b8d | ||
|
|
e348110bdd | ||
|
|
d533423fb6 | ||
|
|
9b4e83705b | ||
|
|
4f6d1d27d5 | ||
|
|
2d9de4703b | ||
|
|
865f4776d0 | ||
|
|
ad6d1202f2 | ||
|
|
3db613da7f | ||
|
|
c48b0d7b99 | ||
|
|
15cdcdfbea | ||
|
|
39b7280595 | ||
|
|
273542f93b | ||
|
|
6d01199185 | ||
|
|
db0ade9791 | ||
|
|
cbad157ded | ||
|
|
a1f6f2fe4c | ||
|
|
f72169d98f | ||
|
|
478e03cbe7 | ||
|
|
c7389cc093 | ||
|
|
e659204e8f | ||
|
|
07e6344812 | ||
|
|
73e50df21b | ||
|
|
80f92203ca | ||
|
|
eab9aca3d4 | ||
|
|
49818cc043 | ||
|
|
51843f364b | ||
|
|
7d8bbe69bd | ||
|
|
6998a11a3a | ||
|
|
65bf81b349 | ||
|
|
65783cde07 | ||
|
|
74297052d4 | ||
|
|
988c3a9f22 | ||
|
|
c08713bfbe | ||
|
|
a10b5a2ee0 | ||
|
|
920ebdde42 | ||
|
|
e31a7eb65f |
File diff suppressed because it is too large
Load Diff
@@ -4,53 +4,54 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.5.10",
|
"@apollo/client": "^3.6.6",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
"@asseinfo/react-kanban": "^2.2.0",
|
||||||
"@craco/craco": "^6.4.3",
|
"@craco/craco": "^6.4.3",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||||
"@sentry/react": "^6.19.6",
|
"@sentry/react": "^7.1.1",
|
||||||
"@sentry/tracing": "^6.19.6",
|
"@sentry/tracing": "^7.1.1",
|
||||||
"@splitsoftware/splitio-react": "^1.3.1-rc.1",
|
"@splitsoftware/splitio-react": "^1.4.1",
|
||||||
"@stripe/react-stripe-js": "^1.7.1",
|
"@stripe/react-stripe-js": "^1.8.1",
|
||||||
"@stripe/stripe-js": "^1.27.0",
|
"@stripe/stripe-js": "^1.31.0",
|
||||||
"@tanem/react-nprogress": "^4.0.12",
|
"@tanem/react-nprogress": "^5.0.1",
|
||||||
"antd": "^4.19.5",
|
"antd": "^4.21.0",
|
||||||
"apollo-link-logger": "^2.0.0",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.27.2",
|
||||||
"craco-less": "^1.20.0",
|
"craco-less": "^1.20.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.1",
|
||||||
"enquire-js": "^0.2.1",
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^9.6.10",
|
"firebase": "^9.8.2",
|
||||||
"graphql": "^16.3.0",
|
"graphql": "^16.5.0",
|
||||||
"i18next": "^21.6.16",
|
"i18next": "^21.8.9",
|
||||||
"i18next-browser-languagedetector": "^6.1.4",
|
"i18next-browser-languagedetector": "^6.1.4",
|
||||||
"jsoneditor": "^9.7.4",
|
"jsoneditor": "^9.8.0",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.9.51",
|
"libphonenumber-js": "^1.10.6",
|
||||||
"logrocket": "^2.2.1",
|
"logrocket": "^3.0.0",
|
||||||
"markerjs2": "^2.21.0",
|
"markerjs2": "^2.21.4",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"moment-timezone": "^0.5.34",
|
"moment-timezone": "^0.5.34",
|
||||||
"phone": "^3.1.15",
|
"normalize-url": "^7.0.3",
|
||||||
|
"phone": "^3.1.20",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"rc-queue-anim": "^2.0.0",
|
"rc-queue-anim": "^2.0.0",
|
||||||
"rc-scroll-anim": "^2.7.6",
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-big-calendar": "^0.38.2",
|
"react-big-calendar": "^0.40.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-drag-listview": "^0.1.9",
|
"react-drag-listview": "^0.2.1",
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^0.5.5",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-i18next": "^11.16.5",
|
"react-i18next": "^11.17.0",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.4.0",
|
||||||
"react-number-format": "^4.9.1",
|
"react-number-format": "^4.9.3",
|
||||||
"react-redux": "^7.2.8",
|
"react-redux": "^7.2.8",
|
||||||
"react-resizable": "^3.0.4",
|
"react-resizable": "^3.0.4",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-router-dom": "^5.3.0",
|
||||||
@@ -58,29 +59,29 @@
|
|||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-sublime-video": "^0.2.5",
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.3",
|
||||||
"recharts": "^2.1.9",
|
"recharts": "^2.1.10",
|
||||||
"redux": "^4.1.2",
|
"redux": "^4.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"redux-state-sync": "^3.1.2",
|
"redux-state-sync": "^3.1.2",
|
||||||
"reselect": "^4.1.5",
|
"reselect": "^4.1.6",
|
||||||
"sass": "^1.50.0",
|
"sass": "^1.51.0",
|
||||||
"socket.io-client": "^4.4.1",
|
"socket.io-client": "^4.5.1",
|
||||||
"styled-components": "^5.3.5",
|
"styled-components": "^5.3.5",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"workbox-background-sync": "^6.5.2",
|
"workbox-background-sync": "^6.5.3",
|
||||||
"workbox-broadcast-update": "^6.5.2",
|
"workbox-broadcast-update": "^6.5.3",
|
||||||
"workbox-cacheable-response": "^6.5.2",
|
"workbox-cacheable-response": "^6.5.3",
|
||||||
"workbox-core": "^6.5.2",
|
"workbox-core": "^6.5.3",
|
||||||
"workbox-expiration": "^6.5.2",
|
"workbox-expiration": "^6.5.3",
|
||||||
"workbox-google-analytics": "^6.5.2",
|
"workbox-google-analytics": "^6.5.3",
|
||||||
"workbox-navigation-preload": "^6.5.2",
|
"workbox-navigation-preload": "^6.5.3",
|
||||||
"workbox-precaching": "^6.5.2",
|
"workbox-precaching": "^6.5.3",
|
||||||
"workbox-range-requests": "^6.5.2",
|
"workbox-range-requests": "^6.5.3",
|
||||||
"workbox-routing": "^6.5.2",
|
"workbox-routing": "^6.5.3",
|
||||||
"workbox-strategies": "^6.5.2",
|
"workbox-strategies": "^6.5.3",
|
||||||
"workbox-streams": "^6.5.2",
|
"workbox-streams": "^6.5.3",
|
||||||
"yauzl": "^2.10.0"
|
"yauzl": "^2.10.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -117,11 +118,11 @@
|
|||||||
"react-error-overlay": "6.0.9"
|
"react-error-overlay": "6.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/webpack-plugin": "^1.18.8",
|
"@sentry/webpack-plugin": "^1.18.9",
|
||||||
"@testing-library/cypress": "^8.0.2",
|
"@testing-library/cypress": "^8.0.2",
|
||||||
"cypress": "^9.5.3",
|
"cypress": "^9.6.1",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"eslint-plugin-cypress": "^2.12.1",
|
||||||
"react-error-overlay": "6.0.10",
|
"react-error-overlay": "6.0.11",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.2"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
client/src/assets/banner4.jpg
Executable file
BIN
client/src/assets/banner4.jpg
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
@@ -13,6 +13,8 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
|
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -27,7 +29,12 @@ export default connect(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(AccountingPayablesTableComponent);
|
)(AccountingPayablesTableComponent);
|
||||||
|
|
||||||
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
|
export function AccountingPayablesTableComponent({
|
||||||
|
bodyshop,
|
||||||
|
loading,
|
||||||
|
bills,
|
||||||
|
refetch,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedBills, setSelectedBills] = useState([]);
|
const [selectedBills, setSelectedBills] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
@@ -131,11 +138,9 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
|
|||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
key: "attempts",
|
key: "attempts",
|
||||||
|
|
||||||
render: (text, record) => {
|
render: (text, record) => (
|
||||||
const success = record.exportlogs.filter((e) => e.successful).length;
|
<ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
const attempts = record.exportlogs.length;
|
),
|
||||||
return `${success}/${attempts}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
@@ -144,14 +149,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
|
|||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<div>
|
<PayableExportButton
|
||||||
<PayableExportButton
|
billId={record.id}
|
||||||
billId={record.id}
|
disabled={transInProgress || !!record.exported}
|
||||||
disabled={transInProgress || !!record.exported}
|
loadingCallback={setTransInProgress}
|
||||||
loadingCallback={setTransInProgress}
|
setSelectedBills={setSelectedBills}
|
||||||
setSelectedBills={setSelectedBills}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -177,11 +181,19 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
|
<BillMarkSelectedExported
|
||||||
|
billids={selectedBills}
|
||||||
|
disabled={transInProgress || selectedBills.length === 0}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
completedCallback={setSelectedBills}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
<PayableExportAll
|
<PayableExportAll
|
||||||
billids={selectedBills}
|
billids={selectedBills}
|
||||||
disabled={transInProgress || selectedBills.length === 0}
|
disabled={transInProgress || selectedBills.length === 0}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedBills}
|
completedCallback={setSelectedBills}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
<QboAuthorizeComponent />
|
<QboAuthorizeComponent />
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -31,6 +32,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
bodyshop,
|
bodyshop,
|
||||||
loading,
|
loading,
|
||||||
payments,
|
payments,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||||
@@ -130,11 +132,9 @@ export function AccountingPayablesTableComponent({
|
|||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
key: "attempts",
|
key: "attempts",
|
||||||
|
|
||||||
render: (text, record) => {
|
render: (text, record) => (
|
||||||
const success = record.exportlogs.filter((e) => e.successful).length;
|
<ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
const attempts = record.exportlogs.length;
|
),
|
||||||
return `${success}/${attempts}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
@@ -148,6 +148,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
disabled={transInProgress || !!record.exportedat}
|
disabled={transInProgress || !!record.exportedat}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
setSelectedPayments={setSelectedPayments}
|
setSelectedPayments={setSelectedPayments}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -188,6 +189,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
disabled={transInProgress || selectedPayments.length === 0}
|
disabled={transInProgress || selectedPayments.length === 0}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedPayments}
|
completedCallback={setSelectedPayments}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
<QboAuthorizeComponent />
|
<QboAuthorizeComponent />
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -30,6 +31,7 @@ export function AccountingReceivablesTableComponent({
|
|||||||
bodyshop,
|
bodyshop,
|
||||||
loading,
|
loading,
|
||||||
jobs,
|
jobs,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||||
@@ -139,12 +141,9 @@ export function AccountingReceivablesTableComponent({
|
|||||||
title: t("exportlogs.labels.attempts"),
|
title: t("exportlogs.labels.attempts"),
|
||||||
dataIndex: "attempts",
|
dataIndex: "attempts",
|
||||||
key: "attempts",
|
key: "attempts",
|
||||||
|
render: (text, record) => (
|
||||||
render: (text, record) => {
|
<ExportLogsCountDisplay logs={record.exportlogs} />
|
||||||
const success = record.exportlogs.filter((e) => e.successful).length;
|
),
|
||||||
const attempts = record.exportlogs.length;
|
|
||||||
return `${success}/${attempts}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
@@ -157,6 +156,7 @@ export function AccountingReceivablesTableComponent({
|
|||||||
jobId={record.id}
|
jobId={record.id}
|
||||||
disabled={!!record.date_exported}
|
disabled={!!record.date_exported}
|
||||||
setSelectedJobs={setSelectedJobs}
|
setSelectedJobs={setSelectedJobs}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||||
@@ -207,6 +207,7 @@ export function AccountingReceivablesTableComponent({
|
|||||||
disabled={transInProgress || selectedJobs.length === 0}
|
disabled={transInProgress || selectedJobs.length === 0}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedJobs}
|
completedCallback={setSelectedJobs}
|
||||||
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||||
|
import "./bill-cm-returns-table.styles.scss";
|
||||||
|
export default function BillCmdReturnsTableComponent({
|
||||||
|
form,
|
||||||
|
loadOutstandingReturns,
|
||||||
|
returnLoading,
|
||||||
|
returnData,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (returnData) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
outstanding_returns: returnData.parts_order_lines,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [returnData, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prev, cur) =>
|
||||||
|
prev.jobid !== cur.jobid ||
|
||||||
|
prev.is_credit_memo !== cur.is_credit_memo ||
|
||||||
|
prev.vendorid !== cur.vendorid
|
||||||
|
}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const isReturn = form.getFieldValue("is_credit_memo");
|
||||||
|
|
||||||
|
if (!isReturn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnLoading) return <Skeleton />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.List name="outstanding_returns">
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("bills.labels.creditsnotreceived")}
|
||||||
|
</Typography.Title>
|
||||||
|
<table className="bill-cm-returns-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("parts_orders.fields.line_desc")}</th>
|
||||||
|
<th>{t("parts_orders.fields.part_type")}</th>
|
||||||
|
<th>{t("parts_orders.fields.quantity")}</th>
|
||||||
|
<th>{t("parts_orders.fields.act_price")}</th>
|
||||||
|
<th>{t("parts_orders.fields.cost")}</th>
|
||||||
|
<th>{t("parts_orders.labels.mark_as_received")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<tr key={field.key}>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
// label={t("joblines.fields.line_desc")}
|
||||||
|
key={`${index}line_desc`}
|
||||||
|
name={[field.name, "line_desc"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}part_type`}
|
||||||
|
name={[field.name, "part_type"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}quantity`}
|
||||||
|
name={[field.name, "quantity"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}act_price`}
|
||||||
|
name={[field.name, "act_price"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}cost`}
|
||||||
|
name={[field.name, "cost"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}cm_received`}
|
||||||
|
name={[field.name, "cm_received"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.bill-cm-returns-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@ export default function BillDeleteButton({ bill }) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await deleteBill({
|
const result = await deleteBill({
|
||||||
variables: { billId: bill.id },
|
variables: { billId: bill.id },
|
||||||
update(cache) {
|
update(cache, { errors }) {
|
||||||
|
if (errors) return;
|
||||||
cache.modify({
|
cache.modify({
|
||||||
fields: {
|
fields: {
|
||||||
bills(existingBills, { readField }) {
|
bills(existingBills, { readField }) {
|
||||||
@@ -36,11 +37,22 @@ export default function BillDeleteButton({ bill }) {
|
|||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("bills.successes.deleted") });
|
notification["success"]({ message: t("bills.successes.deleted") });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
//Check if it's an fkey violation.
|
||||||
message: t("bills.errors.deleting", {
|
const error = JSON.stringify(result.errors);
|
||||||
error: JSON.stringify(result.errors),
|
|
||||||
}),
|
if (error.toLowerCase().includes("inventory_billid_fkey")) {
|
||||||
});
|
notification["error"]({
|
||||||
|
message: t("bills.errors.deleting", {
|
||||||
|
error: t("bills.errors.existinginventoryline"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.deleting", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -12,27 +12,29 @@ import moment from "moment";
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useLocation, useHistory } from "react-router-dom";
|
import { connect } from "react-redux";
|
||||||
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import {
|
import {
|
||||||
DELETE_BILL_LINE,
|
DELETE_BILL_LINE,
|
||||||
INSERT_NEW_BILL_LINES,
|
INSERT_NEW_BILL_LINES,
|
||||||
UPDATE_BILL_LINE,
|
UPDATE_BILL_LINE,
|
||||||
} from "../../graphql/bill-lines.queries";
|
} from "../../graphql/bill-lines.queries";
|
||||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
|
||||||
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|
||||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||||
|
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
|
||||||
|
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||||
|
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) =>
|
||||||
@@ -49,6 +51,7 @@ export default connect(
|
|||||||
export function BillDetailEditcontainer({
|
export function BillDetailEditcontainer({
|
||||||
setPartsOrderContext,
|
setPartsOrderContext,
|
||||||
insertAuditTrail,
|
insertAuditTrail,
|
||||||
|
bodyshop,
|
||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -70,8 +73,8 @@ export function BillDetailEditcontainer({
|
|||||||
sm: "100%",
|
sm: "100%",
|
||||||
md: "100%",
|
md: "100%",
|
||||||
lg: "100%",
|
lg: "100%",
|
||||||
xl: "80%",
|
xl: "90%",
|
||||||
xxl: "80%",
|
xxl: "90%",
|
||||||
};
|
};
|
||||||
const drawerPercentage = selectedBreakpoint
|
const drawerPercentage = selectedBreakpoint
|
||||||
? bpoints[selectedBreakpoint[0]]
|
? bpoints[selectedBreakpoint[0]]
|
||||||
@@ -124,7 +127,7 @@ export function BillDetailEditcontainer({
|
|||||||
});
|
});
|
||||||
|
|
||||||
billlines.forEach((billline) => {
|
billlines.forEach((billline) => {
|
||||||
const { deductedfromlbr, jobline, ...il } = billline;
|
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||||
delete il.__typename;
|
delete il.__typename;
|
||||||
|
|
||||||
if (il.id) {
|
if (il.id) {
|
||||||
@@ -265,12 +268,21 @@ export function BillDetailEditcontainer({
|
|||||||
layout="vertical"
|
layout="vertical"
|
||||||
>
|
>
|
||||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||||
<JobDocumentsGallery
|
|
||||||
jobId={data ? data.bills_by_pk.jobid : null}
|
{bodyshop.uselocalmediaserver ? (
|
||||||
billId={search.billid}
|
<JobsDocumentsLocalGallery
|
||||||
documentsList={data ? data.bills_by_pk.documents : []}
|
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
||||||
billsCallback={refetch}
|
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
||||||
/>
|
vendorid={data ? data.bills_by_pk.vendorid : null}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<JobDocumentsGallery
|
||||||
|
jobId={data ? data.bills_by_pk.jobid : null}
|
||||||
|
billId={search.billid}
|
||||||
|
documentsList={data ? data.bills_by_pk.documents : []}
|
||||||
|
billsCallback={refetch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
UPDATE_JOB,
|
UPDATE_JOB,
|
||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
|
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
||||||
|
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
@@ -23,6 +25,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -47,6 +50,8 @@ function BillEnterModalContainer({
|
|||||||
const [enterAgain, setEnterAgain] = useState(false);
|
const [enterAgain, setEnterAgain] = useState(false);
|
||||||
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
const [insertBill] = useMutation(INSERT_NEW_BILL);
|
||||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
const [updateJobLines] = useMutation(UPDATE_JOB_LINE);
|
||||||
|
const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED);
|
||||||
|
const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
@@ -76,7 +81,13 @@ function BillEnterModalContainer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { upload, location, ...remainingValues } = values;
|
const {
|
||||||
|
upload,
|
||||||
|
location,
|
||||||
|
outstanding_returns,
|
||||||
|
inventory,
|
||||||
|
...remainingValues
|
||||||
|
} = values;
|
||||||
|
|
||||||
let adjustmentsToInsert = {};
|
let adjustmentsToInsert = {};
|
||||||
|
|
||||||
@@ -156,6 +167,25 @@ function BillEnterModalContainer({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const markPolReceived =
|
||||||
|
outstanding_returns &&
|
||||||
|
outstanding_returns.filter((o) => o.cm_received === true);
|
||||||
|
|
||||||
|
if (markPolReceived && markPolReceived.length > 0) {
|
||||||
|
const r2 = await updatePartsOrderLines({
|
||||||
|
variables: { partsLineIds: markPolReceived.map((p) => p.id) },
|
||||||
|
});
|
||||||
|
if (!!r2.errors) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnterAgain(false);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("parts_orders.errors.updating", {
|
||||||
|
message: JSON.stringify(r2.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!!r1.errors) {
|
if (!!r1.errors) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnterAgain(false);
|
setEnterAgain(false);
|
||||||
@@ -167,6 +197,26 @@ function BillEnterModalContainer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const billId = r1.data.insert_bills.returning[0].id;
|
const billId = r1.data.insert_bills.returning[0].id;
|
||||||
|
const markInventoryConsumed =
|
||||||
|
inventory && inventory.filter((i) => i.consumefrominventory);
|
||||||
|
|
||||||
|
if (markInventoryConsumed && markInventoryConsumed.length > 0) {
|
||||||
|
const r2 = await updateInventoryLines({
|
||||||
|
variables: {
|
||||||
|
InventoryIds: markInventoryConsumed.map((p) => p.id),
|
||||||
|
consumedbybillid: billId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!!r2.errors) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnterAgain(false);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("inventory.errors.updating", {
|
||||||
|
message: JSON.stringify(r2.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
remainingValues.billlines
|
remainingValues.billlines
|
||||||
@@ -188,19 +238,33 @@ function BillEnterModalContainer({
|
|||||||
/////////////////////////
|
/////////////////////////
|
||||||
if (upload && upload.length > 0) {
|
if (upload && upload.length > 0) {
|
||||||
//insert Each of the documents?
|
//insert Each of the documents?
|
||||||
upload.forEach((u) => {
|
|
||||||
handleUpload(
|
if (bodyshop.uselocalmediaserver) {
|
||||||
{ file: u.originFileObj },
|
upload.forEach((u) => {
|
||||||
{
|
handleLocalUpload({
|
||||||
bodyshop: bodyshop,
|
ev: { file: u.originFileObj },
|
||||||
uploaded_by: currentUser.email,
|
context: {
|
||||||
jobId: values.jobid,
|
jobid: values.jobid,
|
||||||
billId: billId,
|
invoice_number: remainingValues.invoice_number,
|
||||||
tagsArray: null,
|
vendorid: remainingValues.vendorid,
|
||||||
callback: null,
|
},
|
||||||
}
|
});
|
||||||
);
|
});
|
||||||
});
|
} else {
|
||||||
|
upload.forEach((u) => {
|
||||||
|
handleUpload(
|
||||||
|
{ file: u.originFileObj },
|
||||||
|
{
|
||||||
|
bodyshop: bodyshop,
|
||||||
|
uploaded_by: currentUser.email,
|
||||||
|
jobId: values.jobid,
|
||||||
|
billId: billId,
|
||||||
|
tagsArray: null,
|
||||||
|
callback: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ export function BillFormComponent({
|
|||||||
billEdit,
|
billEdit,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
job,
|
job,
|
||||||
|
loadOutstandingReturns,
|
||||||
|
loadInventory,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -58,6 +60,15 @@ export function BillFormComponent({
|
|||||||
);
|
);
|
||||||
const handleVendorSelect = (props, opt) => {
|
const handleVendorSelect = (props, opt) => {
|
||||||
setDiscount(opt.discount);
|
setDiscount(opt.discount);
|
||||||
|
|
||||||
|
opt &&
|
||||||
|
!billEdit &&
|
||||||
|
loadOutstandingReturns({
|
||||||
|
variables: {
|
||||||
|
jobId: form.getFieldValue("jobid"),
|
||||||
|
vendorId: opt.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,8 +76,8 @@ export function BillFormComponent({
|
|||||||
}, [job, form]);
|
}, [job, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) {
|
const vendorId = form.getFieldValue("vendorid");
|
||||||
const vendorId = form.getFieldValue("vendorid");
|
if (vendorId && vendorAutoCompleteOptions) {
|
||||||
const matchingVendors = vendorAutoCompleteOptions.filter(
|
const matchingVendors = vendorAutoCompleteOptions.filter(
|
||||||
(v) => v.id === vendorId
|
(v) => v.id === vendorId
|
||||||
);
|
);
|
||||||
@@ -74,10 +85,32 @@ export function BillFormComponent({
|
|||||||
setDiscount(matchingVendors[0].discount);
|
setDiscount(matchingVendors[0].discount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (form.getFieldValue("jobid")) {
|
const jobId = form.getFieldValue("jobid");
|
||||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
if (jobId) {
|
||||||
|
loadLines({ variables: { id: jobId } });
|
||||||
|
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
|
||||||
|
loadOutstandingReturns({
|
||||||
|
variables: {
|
||||||
|
jobId: jobId,
|
||||||
|
vendorId: vendorId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [form, setDiscount, vendorAutoCompleteOptions, loadLines]);
|
|
||||||
|
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
|
||||||
|
loadInventory();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
form,
|
||||||
|
billEdit,
|
||||||
|
loadOutstandingReturns,
|
||||||
|
loadInventory,
|
||||||
|
setDiscount,
|
||||||
|
vendorAutoCompleteOptions,
|
||||||
|
loadLines,
|
||||||
|
bodyshop.inhousevendorid,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -107,6 +140,13 @@ export function BillFormComponent({
|
|||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (form.getFieldValue("jobid") !== null) {
|
if (form.getFieldValue("jobid") !== null) {
|
||||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||||
|
if (form.getFieldValue("vendorid") !== null)
|
||||||
|
loadOutstandingReturns({
|
||||||
|
variables: {
|
||||||
|
jobId: form.getFieldValue("jobid"),
|
||||||
|
vendorId: form.getFieldValue("vendorid"),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -228,8 +268,22 @@ export function BillFormComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
value === true &&
|
||||||
|
getFieldValue("jobid") &&
|
||||||
|
getFieldValue("vendorid")
|
||||||
|
) {
|
||||||
|
loadOutstandingReturns({
|
||||||
|
variables: {
|
||||||
|
jobId: form.getFieldValue("jobid"),
|
||||||
|
vendorId: form.getFieldValue("vendorid"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!bodyshop.bill_allow_post_to_closed &&
|
!bodyshop.bill_allow_post_to_closed &&
|
||||||
|
job &&
|
||||||
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
|
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
|
||||||
job.status === bodyshop.md_ro_statuses.default_exported ||
|
job.status === bodyshop.md_ro_statuses.default_exported ||
|
||||||
job.status === bodyshop.md_ro_statuses.default_void) &&
|
job.status === bodyshop.md_ro_statuses.default_void) &&
|
||||||
@@ -380,6 +434,7 @@ export function BillFormComponent({
|
|||||||
form={form}
|
form={form}
|
||||||
responsibilityCenters={responsibilityCenters}
|
responsibilityCenters={responsibilityCenters}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
billEdit={billEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
|||||||
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import BillFormComponent from "./bill-form.component";
|
import BillFormComponent from "./bill-form.component";
|
||||||
|
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
||||||
|
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
||||||
|
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
||||||
|
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -18,6 +23,12 @@ export function BillFormContainer({
|
|||||||
disabled,
|
disabled,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
}) {
|
}) {
|
||||||
|
const { Simple_Inventory } = useTreatments(
|
||||||
|
["Simple_Inventory"],
|
||||||
|
{},
|
||||||
|
bodyshop && bodyshop.imexshopid
|
||||||
|
);
|
||||||
|
|
||||||
const { data: VendorAutoCompleteData } = useQuery(
|
const { data: VendorAutoCompleteData } = useQuery(
|
||||||
SEARCH_VENDOR_AUTOCOMPLETE,
|
SEARCH_VENDOR_AUTOCOMPLETE,
|
||||||
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
|
{ fetchPolicy: "network-only", nextFetchPolicy: "network-only" }
|
||||||
@@ -27,20 +38,45 @@ export function BillFormContainer({
|
|||||||
GET_JOB_LINES_TO_ENTER_BILL
|
GET_JOB_LINES_TO_ENTER_BILL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] =
|
||||||
|
useLazyQuery(QUERY_UNRECEIVED_LINES);
|
||||||
|
const [loadInventory, { loading: inventoryLoading, data: inventoryData }] =
|
||||||
|
useLazyQuery(QUERY_OUTSTANDING_INVENTORY);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BillFormComponent
|
<>
|
||||||
disabled={disabled}
|
<BillFormComponent
|
||||||
form={form}
|
disabled={disabled}
|
||||||
billEdit={billEdit}
|
form={form}
|
||||||
vendorAutoCompleteOptions={
|
billEdit={billEdit}
|
||||||
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
vendorAutoCompleteOptions={
|
||||||
}
|
VendorAutoCompleteData && VendorAutoCompleteData.vendors
|
||||||
loadLines={loadLines}
|
}
|
||||||
lineData={lineData ? lineData.joblines : []}
|
loadLines={loadLines}
|
||||||
job={lineData ? lineData.jobs_by_pk : null}
|
lineData={lineData ? lineData.joblines : []}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
disableInvNumber={disableInvNumber}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
/>
|
disableInvNumber={disableInvNumber}
|
||||||
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
|
loadInventory={loadInventory}
|
||||||
|
/>
|
||||||
|
{!billEdit && (
|
||||||
|
<BillCmdReturnsTableComponent
|
||||||
|
form={form}
|
||||||
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
|
returnLoading={returnLoading}
|
||||||
|
returnData={returnData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{Simple_Inventory.treatment === "on" && (
|
||||||
|
<BillInventoryTable
|
||||||
|
form={form}
|
||||||
|
inventoryLoading={inventoryLoading}
|
||||||
|
inventoryData={billEdit ? [] : inventoryData}
|
||||||
|
billEdit={billEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(BillFormContainer);
|
export default connect(mapStateToProps, null)(BillFormContainer);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -18,6 +18,8 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import CiecaSelect from "../../utils/Ciecaselect";
|
import CiecaSelect from "../../utils/Ciecaselect";
|
||||||
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
|
||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -34,10 +36,16 @@ export function BillEnterModalLinesComponent({
|
|||||||
discount,
|
discount,
|
||||||
form,
|
form,
|
||||||
responsibilityCenters,
|
responsibilityCenters,
|
||||||
|
billEdit,
|
||||||
|
billid,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||||
|
const { Simple_Inventory } = useTreatments(
|
||||||
|
["Simple_Inventory"],
|
||||||
|
{},
|
||||||
|
bodyshop && bodyshop.imexshopid
|
||||||
|
);
|
||||||
const columns = (remove) => {
|
const columns = (remove) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -142,6 +150,24 @@ export function BillEnterModalLinesComponent({
|
|||||||
required: true,
|
required: true,
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
getFieldValue("billlines")[field.fieldKey]?.inventories
|
||||||
|
?.length > value
|
||||||
|
) {
|
||||||
|
return Promise.reject(
|
||||||
|
t("bills.validation.inventoryquantity", {
|
||||||
|
number:
|
||||||
|
getFieldValue("billlines")[field.fieldKey]
|
||||||
|
?.inventories?.length,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -477,9 +503,33 @@ export function BillEnterModalLinesComponent({
|
|||||||
|
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Button disabled={disabled} onClick={() => remove(record.name)}>
|
<Form.Item shouldUpdate noStyle>
|
||||||
<DeleteFilled />
|
{() => (
|
||||||
</Button>
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
disabled ||
|
||||||
|
getFieldValue("billlines")[record.fieldKey]?.inventories
|
||||||
|
?.length > 0
|
||||||
|
}
|
||||||
|
onClick={() => remove(record.name)}
|
||||||
|
>
|
||||||
|
<DeleteFilled />
|
||||||
|
</Button>
|
||||||
|
{Simple_Inventory.treatment === "on" && (
|
||||||
|
<BilllineAddInventory
|
||||||
|
disabled={
|
||||||
|
!billEdit ||
|
||||||
|
form.isFieldsTouched() ||
|
||||||
|
form.getFieldValue("is_credit_memo")
|
||||||
|
}
|
||||||
|
billline={getFieldValue("billlines")[record.fieldKey]}
|
||||||
|
jobid={getFieldValue("jobid")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { Checkbox, Form, Skeleton, Typography } from "antd";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||||
|
import "./bill-inventory-table.styles.scss";
|
||||||
|
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
billEnterModal: selectBillEnterModal,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(BillInventoryTable);
|
||||||
|
|
||||||
|
export function BillInventoryTable({
|
||||||
|
billEnterModal,
|
||||||
|
bodyshop,
|
||||||
|
form,
|
||||||
|
billEdit,
|
||||||
|
inventoryLoading,
|
||||||
|
inventoryData,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inventoryData && inventoryData.inventory) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
inventory: billEnterModal.context.consumeinventoryid
|
||||||
|
? inventoryData.inventory.map((i) => {
|
||||||
|
if (i.id === billEnterModal.context.consumeinventoryid)
|
||||||
|
i.consumefrominventory = true;
|
||||||
|
return i;
|
||||||
|
})
|
||||||
|
: inventoryData.inventory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [inventoryData, form, billEnterModal.context.consumeinventoryid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prev, cur) => prev.vendorid !== cur.vendorid}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const is_inhouse =
|
||||||
|
form.getFieldValue("vendorid") === bodyshop.inhousevendorid;
|
||||||
|
|
||||||
|
if (!is_inhouse || billEdit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inventoryLoading) return <Skeleton />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.List name="inventory">
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("inventory.labels.inventory")}
|
||||||
|
</Typography.Title>
|
||||||
|
<table className="bill-inventory-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("billlines.fields.line_desc")}</th>
|
||||||
|
<th>{t("vendors.fields.name")}</th>
|
||||||
|
<th>{t("billlines.fields.quantity")}</th>
|
||||||
|
<th>{t("billlines.fields.actual_price")}</th>
|
||||||
|
<th>{t("billlines.fields.actual_cost")}</th>
|
||||||
|
<th>{t("inventory.fields.comment")}</th>
|
||||||
|
<th>{t("inventory.actions.consumefrominventory")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<tr key={field.key}>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
// label={t("joblines.fields.line_desc")}
|
||||||
|
key={`${index}line_desc`}
|
||||||
|
name={[field.name, "line_desc"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}part_type`}
|
||||||
|
name={[
|
||||||
|
field.name,
|
||||||
|
"billline",
|
||||||
|
"bill",
|
||||||
|
"vendor",
|
||||||
|
"name",
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}quantity`}
|
||||||
|
name={[field.name, "quantity"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}act_price`}
|
||||||
|
name={[field.name, "actual_price"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}cost`}
|
||||||
|
name={[field.name, "actual_cost"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}comment`}
|
||||||
|
name={[field.name, "comment"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
//label={t("joblines.fields.mod_lb_hrs")}
|
||||||
|
key={`${index}consumefrominventory`}
|
||||||
|
name={[field.name, "consumefrominventory"]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.bill-inventory-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,14 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import {
|
import {
|
||||||
selectAuthLevel,
|
selectAuthLevel,
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
authLevel: selectAuthLevel,
|
authLevel: selectAuthLevel,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
@@ -24,9 +27,15 @@ export default connect(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillMarkExportedButton);
|
)(BillMarkExportedButton);
|
||||||
|
|
||||||
export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
|
export function BillMarkExportedButton({
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
authLevel,
|
||||||
|
bill,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
|
|
||||||
const [updateBill] = useMutation(gql`
|
const [updateBill] = useMutation(gql`
|
||||||
mutation UPDATE_BILL($billId: uuid!) {
|
mutation UPDATE_BILL($billId: uuid!) {
|
||||||
@@ -46,6 +55,20 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
|
|||||||
variables: { billId: bill.id },
|
variables: { billId: bill.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
billid: bill.id,
|
||||||
|
successful: true,
|
||||||
|
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("bills.successes.markexported"),
|
message: t("bills.successes.markexported"),
|
||||||
@@ -69,11 +92,7 @@ export function BillMarkExportedButton({ bodyshop, authLevel, bill }) {
|
|||||||
|
|
||||||
if (hasAccess)
|
if (hasAccess)
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||||
loading={loading}
|
|
||||||
disabled={bill.exported}
|
|
||||||
onClick={handleUpdate}
|
|
||||||
>
|
|
||||||
{t("bills.labels.markexported")}
|
{t("bills.labels.markexported")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { FileAddFilled } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, notification, Tooltip } from "antd";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import moment from "moment";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { INSERT_INVENTORY_AND_CREDIT } from "../../graphql/inventory.queries";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BilllineAddInventory);
|
||||||
|
|
||||||
|
export function BilllineAddInventory({
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
billline,
|
||||||
|
disabled,
|
||||||
|
jobid,
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { billid } = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
|
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||||
|
|
||||||
|
const addToInventory = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
//Check to make sure there are no existing items already in the inventory.
|
||||||
|
|
||||||
|
const cm = {
|
||||||
|
vendorid: bodyshop.inhousevendorid,
|
||||||
|
invoice_number: "ih",
|
||||||
|
jobid: jobid,
|
||||||
|
isinhouse: true,
|
||||||
|
is_credit_memo: true,
|
||||||
|
date: moment().format("YYYY-MM-DD"),
|
||||||
|
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||||
|
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||||
|
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||||
|
total: 0,
|
||||||
|
billlines: [
|
||||||
|
{
|
||||||
|
actual_price: billline.actual_price,
|
||||||
|
actual_cost: billline.actual_cost,
|
||||||
|
quantity: billline.quantity,
|
||||||
|
line_desc: billline.line_desc,
|
||||||
|
cost_center: billline.cost_center,
|
||||||
|
deductedfromlbr: billline.deductedfromlbr,
|
||||||
|
applicable_taxes: {
|
||||||
|
local: billline.applicable_taxes.local,
|
||||||
|
state: billline.applicable_taxes.state,
|
||||||
|
federal: billline.applicable_taxes.federal,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
cm.total = CalculateBillTotal(cm).enteredTotal.getAmount() / 100;
|
||||||
|
|
||||||
|
const insertResult = await insertInventoryLine({
|
||||||
|
variables: {
|
||||||
|
joblineId:
|
||||||
|
billline.joblineid === "noline" ? billline.id : billline.joblineid, //This will return null as there will be no jobline that has the id of the bill line.
|
||||||
|
//Unfortunately, we can't send null as the GQL syntax validation fails.
|
||||||
|
joblineStatus: bodyshop.md_order_statuses.default_returned,
|
||||||
|
inv: {
|
||||||
|
shopid: bodyshop.id,
|
||||||
|
billlineid: billline.id,
|
||||||
|
actual_price: billline.actual_price,
|
||||||
|
actual_cost: billline.actual_cost,
|
||||||
|
quantity: billline.quantity,
|
||||||
|
line_desc: billline.line_desc,
|
||||||
|
},
|
||||||
|
cm: { ...cm, billlines: { data: cm.billlines } }, //Fix structure for apollo insert.
|
||||||
|
pol: {
|
||||||
|
returnfrombill: billid,
|
||||||
|
vendorid: bodyshop.inhousevendorid,
|
||||||
|
deliver_by: moment().format("YYYY-MM-DD"),
|
||||||
|
parts_order_lines: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
line_desc: billline.line_desc,
|
||||||
|
|
||||||
|
act_price: billline.actual_price,
|
||||||
|
cost: billline.actual_cost,
|
||||||
|
quantity: billline.quantity,
|
||||||
|
job_line_id:
|
||||||
|
billline.joblineid === "noline" ? null : billline.joblineid,
|
||||||
|
part_type: billline.jobline && billline.jobline.part_type,
|
||||||
|
cm_received: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
order_date: "2022-06-01",
|
||||||
|
orderedby: currentUser.email,
|
||||||
|
jobid: jobid,
|
||||||
|
user_email: currentUser.email,
|
||||||
|
return: true,
|
||||||
|
status: "Ordered",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
refetchQueries: ["QUERY_BILL_BY_PK"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!insertResult.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
message: t("inventory.successes.inserted"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("inventory.errors.inserting", {
|
||||||
|
error: JSON.stringify(insertResult.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={t("inventory.actions.addtoinventory")}>
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
disabled={
|
||||||
|
disabled || billline?.inventories?.length >= billline.quantity
|
||||||
|
}
|
||||||
|
onClick={addToInventory}
|
||||||
|
>
|
||||||
|
<FileAddFilled />
|
||||||
|
{billline?.inventories?.length > 0 && (
|
||||||
|
<div>({billline?.inventories?.length} in inv)</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ 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 { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
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";
|
||||||
@@ -14,7 +15,7 @@ import BillDeleteButton from "../bill-delete-button/bill-delete-button.component
|
|||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function BillsListTableComponent({
|
export function BillsListTableComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
|
jobRO,
|
||||||
job,
|
job,
|
||||||
billsQuery,
|
billsQuery,
|
||||||
handleOnRowClick,
|
handleOnRowClick,
|
||||||
@@ -43,6 +45,8 @@ export function BillsListTableComponent({
|
|||||||
});
|
});
|
||||||
// const search = queryString.parse(useLocation().search);
|
// const search = queryString.parse(useLocation().search);
|
||||||
// const selectedBill = search.billid;
|
// const selectedBill = search.billid;
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const Templates = TemplateList("bill");
|
const Templates = TemplateList("bill");
|
||||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||||
const { refetch } = billsQuery;
|
const { refetch } = billsQuery;
|
||||||
@@ -56,10 +60,11 @@ export function BillsListTableComponent({
|
|||||||
<BillDeleteButton bill={record} />
|
<BillDeleteButton bill={record} />
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
|
record.is_credit_memo ||
|
||||||
|
record.vendorid === bodyshop.inhousevendorid ||
|
||||||
|
jobRO
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(record);
|
|
||||||
setPartsOrderContext({
|
setPartsOrderContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
context: {
|
context: {
|
||||||
@@ -167,6 +172,24 @@ export function BillsListTableComponent({
|
|||||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredBills = bills
|
||||||
|
? searchText === ""
|
||||||
|
? bills
|
||||||
|
: bills.filter(
|
||||||
|
(b) =>
|
||||||
|
(b.invoice_number || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(b.vendor.name || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(b.total || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t("bills.labels.bills")}
|
title={t("bills.labels.bills")}
|
||||||
@@ -207,8 +230,10 @@ export function BillsListTableComponent({
|
|||||||
|
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
value={searchText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -221,7 +246,7 @@ export function BillsListTableComponent({
|
|||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={bills}
|
dataSource={filteredBills}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||||
form.setFieldsValue({ ca_bc_pvrt: (values.rate * values.days).toFixed(2) });
|
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
|
||||||
setVisibility(false);
|
setVisibility(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
@@ -19,6 +20,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
|
||||||
|
|
||||||
export function ChatMediaSelector({
|
export function ChatMediaSelector({
|
||||||
|
bodyshop,
|
||||||
selectedMedia,
|
selectedMedia,
|
||||||
setSelectedMedia,
|
setSelectedMedia,
|
||||||
conversation,
|
conversation,
|
||||||
@@ -27,7 +29,6 @@ export function ChatMediaSelector({
|
|||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
|
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
@@ -66,6 +67,8 @@ export function ChatMediaSelector({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (bodyshop.uselocalmediaserver) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
|||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
{bodyshop.md_messaging_presets.map((i, idx) => (
|
||||||
<Menu.Item onClick={() => setMessage(i.text)} onItemHover key={idx}>
|
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
|
||||||
{i.label}
|
{i.label}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
|
||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table, Tooltip } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
|
import moment from "moment";
|
||||||
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
sortedInfo: {},
|
sortedInfo: {},
|
||||||
@@ -55,7 +56,25 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
onFilter: (value, record) => value.includes(record.status),
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
render: (text, record) => t(record.status),
|
render: (text, record) => {
|
||||||
|
const { nextservicedate, nextservicekm, mileage } = record;
|
||||||
|
|
||||||
|
const mileageOver = nextservicekm <= mileage;
|
||||||
|
|
||||||
|
const dueForService =
|
||||||
|
nextservicedate && moment(nextservicedate).isBefore(moment());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
{t(record.status)}
|
||||||
|
{(mileageOver || dueForService) && (
|
||||||
|
<Tooltip title={t("contracts.labels.cardueforservice")}>
|
||||||
|
<WarningFilled style={{ color: "tomato" }} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("courtesycars.fields.year"),
|
title: t("courtesycars.fields.year"),
|
||||||
@@ -105,6 +124,17 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
</Link>
|
</Link>
|
||||||
) : null,
|
) : null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("contracts.fields.scheduledreturn"),
|
||||||
|
dataIndex: "scheduledreturn",
|
||||||
|
key: "scheduledreturn",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.cccontracts.length === 1 && (
|
||||||
|
<DateTimeFormatter>
|
||||||
|
{record.cccontracts[0].scheduledreturn}
|
||||||
|
</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { Upload } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
import { handleUpload } from "./documents-local-upload.utility";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function DocumentsLocalUploadComponent({
|
||||||
|
children,
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
job,
|
||||||
|
vendorid,
|
||||||
|
invoice_number,
|
||||||
|
callbackAfterUpload,
|
||||||
|
}) {
|
||||||
|
const [fileList, setFileList] = useState([]);
|
||||||
|
|
||||||
|
const handleDone = (uid) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Upload.Dragger
|
||||||
|
multiple={true}
|
||||||
|
fileList={fileList}
|
||||||
|
onChange={(f) => {
|
||||||
|
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
|
||||||
|
|
||||||
|
setFileList(f.fileList);
|
||||||
|
}}
|
||||||
|
customRequest={(ev) =>
|
||||||
|
handleUpload({
|
||||||
|
ev,
|
||||||
|
context: {
|
||||||
|
jobid: job.id,
|
||||||
|
vendorid,
|
||||||
|
invoice_number,
|
||||||
|
callback: callbackAfterUpload,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
|
||||||
|
>
|
||||||
|
{children || (
|
||||||
|
<>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<UploadOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">
|
||||||
|
Click or drag files to this area to upload.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Upload.Dragger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import cleanAxios from "../../utils/CleanAxios";
|
||||||
|
import { store } from "../../redux/store";
|
||||||
|
import { addMediaForJob } from "../../redux/media/media.actions";
|
||||||
|
import normalizeUrl from "normalize-url";
|
||||||
|
|
||||||
|
export const handleUpload = async ({ ev, context }) => {
|
||||||
|
const { onError, onSuccess, onProgress, file } = ev;
|
||||||
|
const { jobid, invoice_number, vendorid, callbackAfterUpload } = context;
|
||||||
|
|
||||||
|
const bodyshop = store.getState().user.bodyshop;
|
||||||
|
var options = {
|
||||||
|
headers: {
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
ims_token: bodyshop.localmediatoken,
|
||||||
|
},
|
||||||
|
onUploadProgress: (e) => {
|
||||||
|
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("jobid", jobid);
|
||||||
|
if (invoice_number) {
|
||||||
|
formData.append("invoice_number", invoice_number);
|
||||||
|
formData.append("vendorid", vendorid);
|
||||||
|
}
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
const imexMediaServerResponse = await cleanAxios.post(
|
||||||
|
normalizeUrl(
|
||||||
|
`${bodyshop.localmediaserverhttp}/${
|
||||||
|
invoice_number ? "bills" : "jobs"
|
||||||
|
}/upload`
|
||||||
|
),
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imexMediaServerResponse.status !== 200) {
|
||||||
|
if (!!onError) {
|
||||||
|
onError(imexMediaServerResponse.statusText);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onSuccess && onSuccess(file);
|
||||||
|
store.dispatch(
|
||||||
|
addMediaForJob({
|
||||||
|
jobid,
|
||||||
|
media: imexMediaServerResponse.data.map((d) => {
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
selected: false,
|
||||||
|
src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`),
|
||||||
|
thumbnail: normalizeUrl(
|
||||||
|
`${bodyshop.localmediaserverhttp}/${d.thumbnail}`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callbackAfterUpload) {
|
||||||
|
callbackAfterUpload();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -54,7 +54,7 @@ export const uploadToCloudinary = async (
|
|||||||
//Set variables for getting the signed URL.
|
//Set variables for getting the signed URL.
|
||||||
let timestamp = Math.floor(Date.now() / 1000);
|
let timestamp = Math.floor(Date.now() / 1000);
|
||||||
let public_id = key;
|
let public_id = key;
|
||||||
let tags = `${bodyshop.textid},${
|
let tags = `${bodyshop.imexshopid},${
|
||||||
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
|
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
|
||||||
}`;
|
}`;
|
||||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ export function EmailDocumentsComponent({
|
|||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
selectedMedia &&
|
||||||
|
selectedMedia
|
||||||
|
.filter((s) => s.isSelected)
|
||||||
|
.reduce((acc, val) => (acc = acc + val.size), 0)
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
@@ -45,6 +51,12 @@ export function EmailDocumentsComponent({
|
|||||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{selectedMedia &&
|
||||||
|
selectedMedia
|
||||||
|
.filter((s) => s.isSelected)
|
||||||
|
.reduce((acc, val) => (acc = acc + val.size), 0) >= 9961472 ? (
|
||||||
|
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
|
||||||
|
) : null}
|
||||||
{data && (
|
{data && (
|
||||||
<JobDocumentsGalleryExternal
|
<JobDocumentsGalleryExternal
|
||||||
data={data ? data.documents : []}
|
data={data ? data.documents : []}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Menu,
|
Menu,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
Button,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -20,10 +21,13 @@ import {
|
|||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
|
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
|
||||||
|
import { selectEmailConfig } from "../../redux/email/email.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
|
emailConfig: selectEmailConfig,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
@@ -34,6 +38,7 @@ export default connect(
|
|||||||
)(EmailOverlayComponent);
|
)(EmailOverlayComponent);
|
||||||
|
|
||||||
export function EmailOverlayComponent({
|
export function EmailOverlayComponent({
|
||||||
|
emailConfig,
|
||||||
form,
|
form,
|
||||||
selectedMediaState,
|
selectedMediaState,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
@@ -42,7 +47,12 @@ export function EmailOverlayComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleClick = ({ item, key, keyPath }) => {
|
const handleClick = ({ item, key, keyPath }) => {
|
||||||
const email = item.props.value;
|
const email = item.props.value;
|
||||||
form.setFieldsValue({ to: _.uniq([...form.getFieldValue("to"), email]) });
|
form.setFieldsValue({
|
||||||
|
to: _.uniq([
|
||||||
|
...form.getFieldValue("to"),
|
||||||
|
...(typeof email === "string" ? [email] : email),
|
||||||
|
]),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
@@ -55,6 +65,11 @@ export function EmailOverlayComponent({
|
|||||||
{`${e.first_name} ${e.last_name}`}
|
{`${e.first_name} ${e.last_name}`}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
{bodyshop.md_to_emails.map((e, idx) => (
|
||||||
|
<Menu.Item value={e.emails} key={idx + "group"}>
|
||||||
|
{e.label}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -124,7 +139,9 @@ export function EmailOverlayComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider>{t("emails.labels.preview")}</Divider>
|
<Divider>{t("emails.labels.preview")}</Divider>
|
||||||
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
|
{bodyshop.attach_pdf_to_email && (
|
||||||
|
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => {
|
||||||
@@ -143,10 +160,17 @@ export function EmailOverlayComponent({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
|
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
|
||||||
</Tabs.TabPane>
|
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
|
||||||
|
</Tabs.TabPane>
|
||||||
|
)}
|
||||||
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
|
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
|
||||||
|
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
|
||||||
|
<a href={CreateExplorerLinkForJob({ jobid: emailConfig.jobid })}>
|
||||||
|
<Button>{t("documents.labels.openinexplorer")}</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="fileList"
|
name="fileList"
|
||||||
valuePropName="fileList"
|
valuePropName="fileList"
|
||||||
@@ -156,6 +180,23 @@ export function EmailOverlayComponent({
|
|||||||
}
|
}
|
||||||
return e && e.fileList;
|
return e && e.fileList;
|
||||||
}}
|
}}
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
const totalSize = value.reduce(
|
||||||
|
(acc, val) => (acc = acc + val.size),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const limit = 9961472;
|
||||||
|
|
||||||
|
if (totalSize > limit) {
|
||||||
|
return Promise.reject(t("general.errors.sizelimit"));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Upload.Dragger
|
<Upload.Dragger
|
||||||
beforeUpload={Upload.LIST_IGNORE}
|
beforeUpload={Upload.LIST_IGNORE}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ErrorBoundary extends React.Component {
|
|||||||
|
|
||||||
static getDerivedStateFromError(error) {
|
static getDerivedStateFromError(error) {
|
||||||
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
|
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
|
||||||
|
|
||||||
return { hasErrored: true, error: error };
|
return { hasErrored: true, error: error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { WarningOutlined } from "@ant-design/icons";
|
||||||
|
import { Space, Tooltip } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "green",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ExportLogsCountDisplay({ logs }) {
|
||||||
|
const success = logs.filter((e) => e.successful).length;
|
||||||
|
const attempts = logs.length;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Space style={success > 0 ? style : {}}>
|
||||||
|
{`${success}/${attempts}`}
|
||||||
|
{success > 0 && (
|
||||||
|
<Tooltip title={t("exportlogs.labels.priorsuccesfulexport")}>
|
||||||
|
<WarningOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
import Icon, {
|
import Icon, {
|
||||||
BankFilled,
|
BankFilled,
|
||||||
BarChartOutlined,
|
BarChartOutlined,
|
||||||
CarFilled,
|
CarFilled,
|
||||||
ClockCircleFilled,
|
ClockCircleFilled,
|
||||||
|
CheckCircleOutlined,
|
||||||
DashboardFilled,
|
DashboardFilled,
|
||||||
DollarCircleFilled,
|
DollarCircleFilled,
|
||||||
ExportOutlined,
|
ExportOutlined,
|
||||||
@@ -82,6 +84,12 @@ function Header({
|
|||||||
setReportCenterContext,
|
setReportCenterContext,
|
||||||
recentItems,
|
recentItems,
|
||||||
}) {
|
}) {
|
||||||
|
const { Simple_Inventory } = useTreatments(
|
||||||
|
["Simple_Inventory"],
|
||||||
|
{},
|
||||||
|
bodyshop && bodyshop.imexshopid
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -108,6 +116,9 @@ function Header({
|
|||||||
<Menu.Item key="activejobs" icon={<FileFilled />}>
|
<Menu.Item key="activejobs" icon={<FileFilled />}>
|
||||||
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
<Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item key="readyjobs" icon={<CheckCircleOutlined />}>
|
||||||
|
<Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
|
<Menu.Item key="parts-queue" icon={<ToolFilled />}>
|
||||||
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
<Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -195,7 +206,20 @@ function Header({
|
|||||||
>
|
>
|
||||||
{t("menus.header.enterbills")}
|
{t("menus.header.enterbills")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider key="div4" />
|
{Simple_Inventory.treatment === "on" && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider key="div4" />
|
||||||
|
<Menu.Item
|
||||||
|
key="inventory"
|
||||||
|
icon={<Icon component={FaFileInvoiceDollar} />}
|
||||||
|
>
|
||||||
|
<Link to="/manage/inventory">
|
||||||
|
{t("menus.header.inventory")}
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Menu.Divider key="div7" />
|
||||||
<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>
|
||||||
@@ -212,7 +236,6 @@ function Header({
|
|||||||
{t("menus.header.enterpayment")}
|
{t("menus.header.enterpayment")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider key="div5" />
|
<Menu.Divider key="div5" />
|
||||||
|
|
||||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||||
<Link to="/manage/timetickets">
|
<Link to="/manage/timetickets">
|
||||||
{t("menus.header.timetickets")}
|
{t("menus.header.timetickets")}
|
||||||
@@ -231,7 +254,6 @@ function Header({
|
|||||||
{t("menus.header.entertimeticket")}
|
{t("menus.header.entertimeticket")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider key="div6" />
|
<Menu.Divider key="div6" />
|
||||||
|
|
||||||
<Menu.SubMenu
|
<Menu.SubMenu
|
||||||
key="accountingexport"
|
key="accountingexport"
|
||||||
title={t("menus.header.export")}
|
title={t("menus.header.export")}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import moment from "moment";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBillEnterContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(InventoryBillRo);
|
||||||
|
export function InventoryBillRo({
|
||||||
|
bodyshop,
|
||||||
|
setBillEnterContext,
|
||||||
|
inventoryline,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setBillEnterContext({
|
||||||
|
actions: {
|
||||||
|
//refetch: refetch
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
disableInvNumber: true,
|
||||||
|
//job: { id: job.id },
|
||||||
|
consumeinventoryid: inventoryline.id,
|
||||||
|
bill: {
|
||||||
|
vendorid: bodyshop.inhousevendorid,
|
||||||
|
invoice_number: "ih",
|
||||||
|
isinhouse: true,
|
||||||
|
date: moment(),
|
||||||
|
total: 0,
|
||||||
|
billlines: [{}],
|
||||||
|
// billlines: selectedLines.map((p) => {
|
||||||
|
// return {
|
||||||
|
// joblineid: p.id,
|
||||||
|
// actual_price: p.act_price,
|
||||||
|
// actual_cost: 0, //p.act_price,
|
||||||
|
// line_desc: p.line_desc,
|
||||||
|
// line_remarks: p.line_remarks,
|
||||||
|
// part_type: p.part_type,
|
||||||
|
// quantity: p.quantity || 1,
|
||||||
|
// applicable_taxes: {
|
||||||
|
// local: false,
|
||||||
|
// state: false,
|
||||||
|
// federal: false,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("inventory.actions.addtoro")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, notification, Popconfirm } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DELETE_INVENTORY_LINE } from "../../graphql/inventory.queries";
|
||||||
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|
||||||
|
export default function InventoryLineDelete({
|
||||||
|
inventoryline,
|
||||||
|
disabled,
|
||||||
|
refetch,
|
||||||
|
}) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [deleteInventoryLine] = useMutation(DELETE_INVENTORY_LINE);
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await deleteInventoryLine({
|
||||||
|
variables: { lineId: inventoryline.id },
|
||||||
|
// update(cache, { errors }) {
|
||||||
|
// cache.modify({
|
||||||
|
// fields: {
|
||||||
|
// inventory(existingInventory, { readField }) {
|
||||||
|
// console.log(existingInventory);
|
||||||
|
// return existingInventory.filter(
|
||||||
|
// (invRef) => inventoryline.id !== readField("id", invRef)
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!!result.errors) {
|
||||||
|
notification["success"]({ message: t("inventory.successes.deleted") });
|
||||||
|
} else {
|
||||||
|
//Check if it's an fkey violation.
|
||||||
|
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.deleting", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (refetch) refetch();
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RbacWrapper action="inventory:delete" noauth={<></>}>
|
||||||
|
<Popconfirm
|
||||||
|
disabled={disabled || inventoryline.consumedbybillid}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
title={t("inventory.labels.deleteconfirm")}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
disabled={disabled || inventoryline.consumedbybillid}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<DeleteFilled />
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</RbacWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { EditFilled, SyncOutlined, FileAddFilled } from "@ant-design/icons";
|
||||||
|
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import InventoryBillRo from "../inventory-bill-ro/inventory-bill-ro.component";
|
||||||
|
import InventoryLineDelete from "../inventory-line-delete/inventory-line-delete.component";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setInventoryUpsertContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "inventoryUpsert" })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JobsList({
|
||||||
|
bodyshop,
|
||||||
|
refetch,
|
||||||
|
loading,
|
||||||
|
jobs,
|
||||||
|
total,
|
||||||
|
setInventoryUpsertContext,
|
||||||
|
}) {
|
||||||
|
const search = queryString.parse(useLocation().search);
|
||||||
|
const { page, sortcolumn, sortorder } = search;
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("billlines.fields.line_desc"),
|
||||||
|
dataIndex: "line_desc",
|
||||||
|
key: "line_desc",
|
||||||
|
|
||||||
|
sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||||
|
sortOrder: sortcolumn === "line_desc" && sortorder,
|
||||||
|
render: (text, record) =>
|
||||||
|
record.billline?.bill?.job ? (
|
||||||
|
<div>
|
||||||
|
<div>{text}</div>
|
||||||
|
<strong>{`(${record.billline?.bill?.job?.v_model_yr} ${record.billline?.bill?.job?.v_make_desc} ${record.billline?.bill?.job?.v_model_desc})`}</strong>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
text
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("inventory.labels.frombillinvoicenumber"),
|
||||||
|
dataIndex: "vendorname",
|
||||||
|
key: "vendorname",
|
||||||
|
ellipsis: true,
|
||||||
|
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
|
|
||||||
|
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||||
|
render: (text, record) =>
|
||||||
|
(
|
||||||
|
(record.billline?.bill?.invoice_number || "") +
|
||||||
|
" " +
|
||||||
|
(record.manualinvoicenumber || "")
|
||||||
|
).trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("inventory.labels.fromvendor"),
|
||||||
|
dataIndex: "vendorname",
|
||||||
|
key: "vendorname",
|
||||||
|
ellipsis: true,
|
||||||
|
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
|
|
||||||
|
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||||
|
render: (text, record) =>
|
||||||
|
(
|
||||||
|
(record.billline?.bill?.vendor?.name || "") +
|
||||||
|
" " +
|
||||||
|
(record.manualvendor || "")
|
||||||
|
).trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("billlines.fields.actual_price"),
|
||||||
|
dataIndex: "actual_price",
|
||||||
|
key: "actual_price",
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("billlines.fields.actual_cost"),
|
||||||
|
dataIndex: "actual_cost",
|
||||||
|
key: "actual_cost",
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("inventory.fields.comment"),
|
||||||
|
dataIndex: "comment",
|
||||||
|
key: "comment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("inventory.labels.consumedbyjob"),
|
||||||
|
dataIndex: "consumedbyjob",
|
||||||
|
key: "consumedbyjob",
|
||||||
|
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) =>
|
||||||
|
record.bill?.job?.ro_number ? (
|
||||||
|
<Link to={`/manage/jobs/${record.bill?.job?.id}`}>
|
||||||
|
{record.bill?.job?.ro_number}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<InventoryBillRo inventoryline={record} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("general.labels.actions"),
|
||||||
|
dataIndex: "actions",
|
||||||
|
key: "actions",
|
||||||
|
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) => (
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setInventoryUpsertContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
existingInventory: record,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditFilled />
|
||||||
|
</Button>
|
||||||
|
<InventoryLineDelete inventoryline={record} refetch={refetch} />
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
search.page = pagination.current;
|
||||||
|
search.sortcolumn = sorter.column && sorter.column.key;
|
||||||
|
search.sortorder = sorter.order;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
{search.search && (
|
||||||
|
<>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("general.labels.searchresults", { search: search.search })}
|
||||||
|
</Typography.Title>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
delete search.search;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("general.actions.clear")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setInventoryUpsertContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileAddFilled />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (search.showall) delete search.showall;
|
||||||
|
else {
|
||||||
|
search.showall = true;
|
||||||
|
}
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{search.showall
|
||||||
|
? t("inventory.labels.showavailable")
|
||||||
|
: t("inventory.labels.showall")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={() => refetch()}>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
<Input.Search
|
||||||
|
placeholder={search.search || t("general.labels.search")}
|
||||||
|
onSearch={(value) => {
|
||||||
|
search.search = value;
|
||||||
|
history.push({ search: queryString.stringify(search) });
|
||||||
|
}}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
position: "top",
|
||||||
|
pageSize: 25,
|
||||||
|
current: parseInt(page || 1),
|
||||||
|
total: total,
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={jobs}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsList);
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { QUERY_INVENTORY_PAGINATED } from "../../graphql/inventory.queries";
|
||||||
|
import {
|
||||||
|
setBreadcrumbs,
|
||||||
|
setSelectedHeader,
|
||||||
|
} from "../../redux/application/application.actions";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import InventoryListPaginated from "./inventory-list.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
|
||||||
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
|
const { page, sortcolumn, sortorder, search, showall } = searchParams;
|
||||||
|
|
||||||
|
const { loading, error, data, refetch } = useQuery(
|
||||||
|
QUERY_INVENTORY_PAGINATED,
|
||||||
|
{
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
variables: {
|
||||||
|
search: search || "",
|
||||||
|
offset: page ? (page - 1) * 25 : 0,
|
||||||
|
limit: 25,
|
||||||
|
consumedIsNull: showall === "true" ? null : true,
|
||||||
|
order: [
|
||||||
|
{
|
||||||
|
[sortcolumn || "created_at"]:
|
||||||
|
sortorder && sortorder !== "false"
|
||||||
|
? sortorder === "descend"
|
||||||
|
? "desc"
|
||||||
|
: "asc"
|
||||||
|
: "desc",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
return (
|
||||||
|
<InventoryListPaginated
|
||||||
|
refetch={refetch}
|
||||||
|
loading={loading}
|
||||||
|
searchParams={searchParams}
|
||||||
|
total={data ? data.search_inventory_aggregate.aggregate.count : 0}
|
||||||
|
jobs={data ? data.search_inventory : []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Form, Input, Space } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
|
||||||
|
import FormItemCurrency from "../form-items-formatted/currency-form-item.component";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
inventoryUpsertModal: selectInventoryUpsert,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(NoteUpsertModalComponent);
|
||||||
|
|
||||||
|
export function NoteUpsertModalComponent({ form, inventoryUpsertModal }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { existingInventory } = inventoryUpsertModal.context;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<Form.Item
|
||||||
|
label={t("billlines.fields.line_desc")}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
name="line_desc"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("inventory.fields.comment")} name="comment">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{!existingInventory && (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label={t("inventory.fields.manualinvoicenumber")}
|
||||||
|
name="manualinvoicenumber"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("inventory.fields.manualvendor")}
|
||||||
|
name="manualvendor"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
label={t("billlines.fields.actual_cost")}
|
||||||
|
name="actual_cost"
|
||||||
|
>
|
||||||
|
<FormItemCurrency />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
label={t("billlines.fields.actual_price")}
|
||||||
|
name="actual_price"
|
||||||
|
>
|
||||||
|
<FormItemCurrency />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Form, Modal, notification } from "antd";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import {
|
||||||
|
INSERT_INVENTORY_LINE,
|
||||||
|
UPDATE_INVENTORY_LINE,
|
||||||
|
} from "../../graphql/inventory.queries";
|
||||||
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectInventoryUpsert } from "../../redux/modals/modals.selectors";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
import InventoryUpsertModal from "./inventory-upsert-modal.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
inventoryUpsertModal: selectInventoryUpsert,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
toggleModalVisible: () => dispatch(toggleModalVisible("inventoryUpsert")),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function InventoryUpsertModalContainer({
|
||||||
|
currentUser,
|
||||||
|
bodyshop,
|
||||||
|
inventoryUpsertModal,
|
||||||
|
toggleModalVisible,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [insertInventory] = useMutation(INSERT_INVENTORY_LINE);
|
||||||
|
const [updateInventoryLine] = useMutation(UPDATE_INVENTORY_LINE);
|
||||||
|
|
||||||
|
const { visible, context, actions } = inventoryUpsertModal;
|
||||||
|
const { existingInventory } = context;
|
||||||
|
const { refetch } = actions;
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//Required to prevent infinite looping.
|
||||||
|
if (existingInventory && visible) {
|
||||||
|
form.setFieldsValue(existingInventory);
|
||||||
|
} else if (!existingInventory && visible) {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
}, [existingInventory, form, visible]);
|
||||||
|
|
||||||
|
const handleFinish = async (formValues) => {
|
||||||
|
const values = formValues;
|
||||||
|
|
||||||
|
if (existingInventory) {
|
||||||
|
logImEXEvent("inventory_update");
|
||||||
|
|
||||||
|
updateInventoryLine({
|
||||||
|
variables: {
|
||||||
|
inventoryId: existingInventory.id,
|
||||||
|
inventoryItem: values,
|
||||||
|
},
|
||||||
|
}).then((r) => {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("inventory.successes.updated"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// if (refetch) refetch();
|
||||||
|
toggleModalVisible();
|
||||||
|
} else {
|
||||||
|
logImEXEvent("inventory_insert");
|
||||||
|
|
||||||
|
await insertInventory({
|
||||||
|
variables: {
|
||||||
|
inventoryItem: { shopid: bodyshop.id, ...values },
|
||||||
|
},
|
||||||
|
update(cache, { data }) {
|
||||||
|
cache.modify({
|
||||||
|
fields: {
|
||||||
|
inventory(existingInv) {
|
||||||
|
return [...existingInv, data.insert_inventory_one];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refetch) refetch();
|
||||||
|
form.resetFields();
|
||||||
|
toggleModalVisible();
|
||||||
|
notification["success"]({
|
||||||
|
message: t("inventory.successes.inserted"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
existingInventory
|
||||||
|
? t("inventory.actions.edit")
|
||||||
|
: t("inventory.actions.new")
|
||||||
|
}
|
||||||
|
visible={visible}
|
||||||
|
okText={t("general.actions.save")}
|
||||||
|
onOk={() => {
|
||||||
|
form.submit();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
toggleModalVisible();
|
||||||
|
}}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
|
<InventoryUpsertModal form={form} />
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(InventoryUpsertModalContainer);
|
||||||
@@ -49,11 +49,11 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
|||||||
|
|
||||||
GenerateDocument(
|
GenerateDocument(
|
||||||
{
|
{
|
||||||
name: TemplateList("job_special").thirdpartypayer.key,
|
name: TemplateList("job_special").special_thirdpartypayer.key,
|
||||||
variables: { id: jobId },
|
variables: { id: jobId },
|
||||||
context: restVals,
|
context: restVals,
|
||||||
},
|
},
|
||||||
{ subject: TemplateList("job_special").thirdpartypayer.subject },
|
{ subject: TemplateList("job_special").special_thirdpartypayer.subject },
|
||||||
sendtype
|
sendtype
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export default function JobBillsTotalComponent({
|
|||||||
let billCms = Dinero();
|
let billCms = Dinero();
|
||||||
let lbrAdjustments = Dinero();
|
let lbrAdjustments = Dinero();
|
||||||
let totalReturns = Dinero();
|
let totalReturns = Dinero();
|
||||||
|
let totalReturnsMarkedNotReceived = Dinero();
|
||||||
|
let totalReturnsMarkedReceived = Dinero();
|
||||||
|
|
||||||
partsOrders.forEach((p) =>
|
partsOrders.forEach((p) =>
|
||||||
p.parts_order_lines.forEach((pol) => {
|
p.parts_order_lines.forEach((pol) => {
|
||||||
@@ -35,6 +37,24 @@ export default function JobBillsTotalComponent({
|
|||||||
amount: Math.round((pol.act_price || 0) * 100),
|
amount: Math.round((pol.act_price || 0) * 100),
|
||||||
}).multiply(pol.quantity)
|
}).multiply(pol.quantity)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (pol.cm_received === null) {
|
||||||
|
return; // Skip this calculation for bills posted prior to the CNR change.
|
||||||
|
} else {
|
||||||
|
if (pol.cm_received === false) {
|
||||||
|
totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add(
|
||||||
|
Dinero({
|
||||||
|
amount: Math.round((pol.act_price || 0) * 100),
|
||||||
|
}).multiply(pol.quantity)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
totalReturnsMarkedReceived = totalReturnsMarkedReceived.add(
|
||||||
|
Dinero({
|
||||||
|
amount: Math.round((pol.act_price || 0) * 100),
|
||||||
|
}).multiply(pol.quantity)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -66,6 +86,7 @@ export default function JobBillsTotalComponent({
|
|||||||
|
|
||||||
const totalPartsSublet = Dinero(totals.parts.parts.total)
|
const totalPartsSublet = Dinero(totals.parts.parts.total)
|
||||||
.add(Dinero(totals.parts.sublets.total))
|
.add(Dinero(totals.parts.sublets.total))
|
||||||
|
.add(Dinero(totals.additional.shipping))
|
||||||
.add(Dinero(totals.additional.towing));
|
.add(Dinero(totals.additional.towing));
|
||||||
|
|
||||||
const discrepancy = totalPartsSublet.subtract(billTotals);
|
const discrepancy = totalPartsSublet.subtract(billTotals);
|
||||||
@@ -73,7 +94,7 @@ export default function JobBillsTotalComponent({
|
|||||||
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
|
const discrepWithLbrAdj = discrepancy.add(lbrAdjustments);
|
||||||
|
|
||||||
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
|
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
|
||||||
const creditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
|
const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
@@ -213,6 +234,32 @@ export default function JobBillsTotalComponent({
|
|||||||
value={totalReturns.toFormat()}
|
value={totalReturns.toFormat()}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t(
|
||||||
|
"jobs.labels.plitooltips.calculatedcreditsnotreceived"
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Statistic
|
||||||
|
title={t("bills.labels.calculatedcreditsnotreceived")}
|
||||||
|
valueStyle={{
|
||||||
|
color:
|
||||||
|
calculatedCreditsNotReceived.getAmount() <= 0
|
||||||
|
? "green"
|
||||||
|
: "red",
|
||||||
|
}}
|
||||||
|
value={
|
||||||
|
calculatedCreditsNotReceived.getAmount() >= 0
|
||||||
|
? calculatedCreditsNotReceived.toFormat()
|
||||||
|
: Dinero().toFormat()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<div
|
<div
|
||||||
@@ -225,11 +272,14 @@ export default function JobBillsTotalComponent({
|
|||||||
<Statistic
|
<Statistic
|
||||||
title={t("bills.labels.creditsnotreceived")}
|
title={t("bills.labels.creditsnotreceived")}
|
||||||
valueStyle={{
|
valueStyle={{
|
||||||
color: creditsNotReceived.getAmount() <= 0 ? "green" : "red",
|
color:
|
||||||
|
totalReturnsMarkedNotReceived.getAmount() <= 0
|
||||||
|
? "green"
|
||||||
|
: "red",
|
||||||
}}
|
}}
|
||||||
value={
|
value={
|
||||||
creditsNotReceived.getAmount() >= 0
|
totalReturnsMarkedNotReceived.getAmount() >= 0
|
||||||
? creditsNotReceived.toFormat()
|
? totalReturnsMarkedNotReceived.toFormat()
|
||||||
: Dinero().toFormat()
|
: Dinero().toFormat()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import React from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
import JobSyncButton from "../job-sync-button/job-sync-button.component";
|
||||||
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
|
||||||
@@ -20,6 +22,10 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
|
|||||||
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
|
||||||
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) =>
|
setPrintCenterContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
||||||
@@ -31,7 +37,7 @@ const span = {
|
|||||||
lg: { span: 8 },
|
lg: { span: 8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function JobDetailCards({ setPrintCenterContext }) {
|
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
.slice(-1)[0];
|
.slice(-1)[0];
|
||||||
@@ -143,12 +149,14 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<JobDetailCardsDocumentsComponent
|
<Col {...span}>
|
||||||
loading={loading}
|
<JobDetailCardsDocumentsComponent
|
||||||
data={data ? data.jobs_by_pk : null}
|
loading={loading}
|
||||||
/>
|
data={data ? data.jobs_by_pk : null}
|
||||||
</Col>
|
/>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<JobDetailCardsDamageComponent
|
<JobDetailCardsDamageComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@@ -161,4 +169,4 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(null, mapDispatchToProps)(JobDetailCards);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobDetailCards);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
EditFilled,
|
EditFilled,
|
||||||
PlusCircleTwoTone,
|
PlusCircleTwoTone,
|
||||||
MinusCircleTwoTone,
|
MinusCircleTwoTone,
|
||||||
|
HomeOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +43,7 @@ import _ from "lodash";
|
|||||||
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||||
import JobLinesExpander from "./job-lines-expander.component";
|
import JobLinesExpander from "./job-lines-expander.component";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -54,6 +56,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
|
setBillEnterContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobLinesComponent({
|
export function JobLinesComponent({
|
||||||
@@ -68,6 +72,7 @@ export function JobLinesComponent({
|
|||||||
job,
|
job,
|
||||||
setJobLineEditContext,
|
setJobLineEditContext,
|
||||||
form,
|
form,
|
||||||
|
setBillEnterContext,
|
||||||
}) {
|
}) {
|
||||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||||
|
|
||||||
@@ -386,6 +391,62 @@ export function JobLinesComponent({
|
|||||||
</Space>
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
(job && !job.converted) ||
|
||||||
|
(selectedLines.length > 0 ? false : true) ||
|
||||||
|
jobRO ||
|
||||||
|
technician
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
// setPartsOrderContext({
|
||||||
|
// actions: { refetch: refetch },
|
||||||
|
// context: {
|
||||||
|
// jobId: job.id,
|
||||||
|
// job: job,
|
||||||
|
// linesToOrder: selectedLines,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
setBillEnterContext({
|
||||||
|
actions: { refetch: refetch },
|
||||||
|
context: {
|
||||||
|
disableInvNumber: true,
|
||||||
|
job: { id: job.id },
|
||||||
|
bill: {
|
||||||
|
vendorid: bodyshop.inhousevendorid,
|
||||||
|
invoice_number: "ih",
|
||||||
|
isinhouse: true,
|
||||||
|
date: new moment(),
|
||||||
|
total: 0,
|
||||||
|
billlines: selectedLines.map((p) => {
|
||||||
|
return {
|
||||||
|
joblineid: p.id,
|
||||||
|
actual_price: p.act_price,
|
||||||
|
actual_cost: 0, //p.act_price,
|
||||||
|
line_desc: p.line_desc,
|
||||||
|
line_remarks: p.line_remarks,
|
||||||
|
part_type: p.part_type,
|
||||||
|
quantity: p.quantity || 1,
|
||||||
|
applicable_taxes: {
|
||||||
|
local: false,
|
||||||
|
state: false,
|
||||||
|
federal: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//Clear out the selected lines. IO-785
|
||||||
|
setSelectedLines([]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HomeOutlined />
|
||||||
|
{t("parts.actions.orderinhouse")}
|
||||||
|
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
(job && !job.converted) ||
|
(job && !job.converted) ||
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function JoblinePresetButton({ bodyshop, form }) {
|
|||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
{bodyshop.md_jobline_presets.map((i, idx) => (
|
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||||
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
|
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
|
||||||
{i.label}
|
{i.label}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ export function JobLinesUpsertModalComponent({
|
|||||||
rules={[
|
rules={[
|
||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator(rule, value) {
|
validator(rule, value) {
|
||||||
if (!!getFieldValue("mod_lbr_ty") === !!value) {
|
if (
|
||||||
|
!!getFieldValue("mod_lbr_ty") === (!!value || value === 0)
|
||||||
|
) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Row, Col, Tag, Tooltip } from "antd";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
|
||||||
|
|
||||||
|
export function JobPartsQueueCount({ bodyshop, parts, style }) {
|
||||||
|
const partsStatus = useMemo(() => {
|
||||||
|
if (!parts) return null;
|
||||||
|
return parts.reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
|
||||||
|
acc.total = acc.total + val.count;
|
||||||
|
acc[val.status] = acc[val.status] + val.count;
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
total: 0,
|
||||||
|
null: 0,
|
||||||
|
[bodyshop.md_order_statuses.default_bo]: 0,
|
||||||
|
[bodyshop.md_order_statuses.default_ordered]: 0,
|
||||||
|
[bodyshop.md_order_statuses.default_received]: 0,
|
||||||
|
[bodyshop.md_order_statuses.default_returned]: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [bodyshop, parts]);
|
||||||
|
|
||||||
|
if (!parts) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row style={style}>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tooltip title="Total">
|
||||||
|
<Tag>{partsStatus.total}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tooltip title="No Status">
|
||||||
|
<Tag color="gold">{partsStatus["null"]}</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
|
||||||
|
<Tag color="blue">
|
||||||
|
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tooltip title={bodyshop.md_order_statuses.default_received}>
|
||||||
|
<Tag color="green">
|
||||||
|
{partsStatus[bodyshop.md_order_statuses.default_received]}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
|
||||||
|
<Tag color="orange">
|
||||||
|
{partsStatus[bodyshop.md_order_statuses.default_returned]}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
<Col span={4}>
|
||||||
|
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
|
||||||
|
<Tag color="red">
|
||||||
|
{partsStatus[bodyshop.md_order_statuses.default_bo]}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,7 +22,8 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
|||||||
(j.part_type !== null && j.part_type !== "PAE") ||
|
(j.part_type !== null && j.part_type !== "PAE") ||
|
||||||
(j.line_desc &&
|
(j.line_desc &&
|
||||||
j.line_desc.toLowerCase().includes("towing") &&
|
j.line_desc.toLowerCase().includes("towing") &&
|
||||||
j.lbr_op === "OP13")
|
j.lbr_op === "OP13") ||
|
||||||
|
j.db_ref === "936004" //ADD SHIPPING LINE.
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
export const reconcileByAssocLine = (
|
export const reconcileByAssocLine = (
|
||||||
jobLines,
|
jobLines,
|
||||||
jobLineState,
|
jobLineState,
|
||||||
@@ -73,7 +74,12 @@ export const reconcileByPrice = (
|
|||||||
|
|
||||||
jobLines.forEach((jl) => {
|
jobLines.forEach((jl) => {
|
||||||
const matchingBillLineIds = billLines
|
const matchingBillLineIds = billLines
|
||||||
.filter((bl) => bl.actual_price === jl.act_price && bl.quantity === jl.part_qty && !jl.removed)
|
.filter(
|
||||||
|
(bl) =>
|
||||||
|
bl.actual_price === jl.act_price &&
|
||||||
|
bl.quantity === jl.part_qty &&
|
||||||
|
!jl.removed
|
||||||
|
)
|
||||||
.map((bl) => bl.id);
|
.map((bl) => bl.id);
|
||||||
|
|
||||||
if (matchingBillLineIds.length > 1) {
|
if (matchingBillLineIds.length > 1) {
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { Button, notification } from "antd";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { Checkbox, notification, Space, Spin } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
|
||||||
export default function JobRemoveFromPartsQueue({ jobId, refetch }) {
|
export default function JobRemoveFromPartsQueue({ checked, jobId }) {
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleClick = async (e) => {
|
const handleChange = async (e) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateJob({
|
const result = await updateJob({
|
||||||
variables: { jobId: jobId, job: { queued_for_parts: false } },
|
variables: { jobId: jobId, job: { queued_for_parts: e.target.checked } },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
if (refetch) refetch();
|
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
@@ -30,8 +29,9 @@ export default function JobRemoveFromPartsQueue({ jobId, refetch }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleClick} loading={loading}>
|
<Space>
|
||||||
{t("general.actions.remove")}
|
<Checkbox checked={checked} onChange={handleChange} />
|
||||||
</Button>
|
{loading && <Spin size="small" />}
|
||||||
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export default function ScoreboardAddButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={overlay} visible={visibility}>
|
<Popover content={overlay} visible={visibility} placement="bottom">
|
||||||
<Button
|
<Button
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { LoadingOutlined } from "@ant-design/icons";
|
|
||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { Empty, Select, Space, Tag } from "antd";
|
import { Select, Space, Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { forwardRef, useEffect } from "react";
|
import React, { forwardRef, useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
|
SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE,
|
||||||
@@ -24,31 +23,35 @@ const JobSearchSelect = (
|
|||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [theOptions, setTheOptions] = useState([]);
|
||||||
const [callSearch, { loading, error, data }] = useLazyQuery(
|
const [callSearch, { loading, error, data }] = useLazyQuery(
|
||||||
SEARCH_JOBS_FOR_AUTOCOMPLETE,
|
SEARCH_JOBS_FOR_AUTOCOMPLETE,
|
||||||
{
|
{}
|
||||||
...(convertedOnly || notExported
|
|
||||||
? {
|
|
||||||
variables: {
|
|
||||||
...(convertedOnly ? { isConverted: true } : {}),
|
|
||||||
...(notExported ? { notExported: true } : {}),
|
|
||||||
...(notInvoiced ? { notInvoiced: true } : {}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
|
const [callIdSearch, { loading: idLoading, error: idError, data: idData }] =
|
||||||
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
|
useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE);
|
||||||
|
|
||||||
const executeSearch = (v) => {
|
const executeSearch = (v) => {
|
||||||
callSearch(v);
|
if (v && v !== "") callSearch(v);
|
||||||
};
|
};
|
||||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||||
|
|
||||||
const handleSearch = (value) => {
|
const handleSearch = (value) => {
|
||||||
debouncedExecuteSearch({ variables: { search: value } });
|
debouncedExecuteSearch({
|
||||||
|
variables: {
|
||||||
|
search: value,
|
||||||
|
...(convertedOnly || notExported
|
||||||
|
? {
|
||||||
|
variables: {
|
||||||
|
...(convertedOnly ? { isConverted: true } : {}),
|
||||||
|
...(notExported ? { notExported: true } : {}),
|
||||||
|
...(notInvoiced ? { notInvoiced: true } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,13 +60,17 @@ const JobSearchSelect = (
|
|||||||
}
|
}
|
||||||
}, [restProps.value, callIdSearch]);
|
}, [restProps.value, callIdSearch]);
|
||||||
|
|
||||||
const theOptions = _.uniqBy(
|
useEffect(() => {
|
||||||
[
|
setTheOptions(
|
||||||
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
|
_.uniqBy(
|
||||||
...(data && data.search_jobs ? data.search_jobs : []),
|
[
|
||||||
],
|
...(idData && idData.jobs_by_pk ? [idData.jobs_by_pk] : []),
|
||||||
"id"
|
...(data && data.search_jobs ? data.search_jobs : []),
|
||||||
);
|
],
|
||||||
|
"id"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [data, idData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -77,7 +84,8 @@ const JobSearchSelect = (
|
|||||||
}}
|
}}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
loading={loading || idLoading}
|
||||||
|
//notFoundContent={loading ? <LoadingOutlined /> : <Empty />}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{theOptions
|
{theOptions
|
||||||
@@ -99,7 +107,6 @@ const JobSearchSelect = (
|
|||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
</Select>
|
</Select>
|
||||||
{idLoading || loading ? <LoadingOutlined /> : null}
|
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{idError ? (
|
{idError ? (
|
||||||
<AlertComponent message={idError.message} type="error" />
|
<AlertComponent message={idError.message} type="error" />
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||||
|
|
||||||
export function JobsAdminStatus({ bodyshop, job }) {
|
export function JobsAdminStatus({ insertAuditTrail, bodyshop, job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||||
@@ -26,6 +29,10 @@ export function JobsAdminStatus({ bodyshop, job }) {
|
|||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.admin_jobstatuschange(status),
|
||||||
|
});
|
||||||
// refetch();
|
// refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -7,8 +7,27 @@ import DateTimePicker from "../form-date-time-picker/form-date-time-picker.compo
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
export default function JobsAdminDatesChange({ job }) {
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobsAdminDatesChange);
|
||||||
|
|
||||||
|
export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
@@ -20,6 +39,23 @@ export default function JobsAdminDatesChange({ job }) {
|
|||||||
variables: { jobId: job.id, job: values },
|
variables: { jobId: job.id, job: values },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changedAuditFields = form.getFieldsValue(
|
||||||
|
true,
|
||||||
|
(meta) => meta && meta.touched
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.keys(changedAuditFields).forEach((key) => {
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.admin_jobfieldchange(
|
||||||
|
key,
|
||||||
|
changedAuditFields[key] instanceof moment
|
||||||
|
? moment(changedAuditFields[key]).format("MM/DD/YYYY hh:mm a")
|
||||||
|
: changedAuditFields[key]
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,22 +6,36 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(JobAdminMarkReexport);
|
)(JobAdminMarkReexport);
|
||||||
|
|
||||||
export function JobAdminMarkReexport({ bodyshop, job }) {
|
export function JobAdminMarkReexport({
|
||||||
|
insertAuditTrail,
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
job,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
const [markJobForReexport] = useMutation(gql`
|
const [markJobForReexport] = useMutation(gql`
|
||||||
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
|
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
|
||||||
update_jobs_by_pk(
|
update_jobs_by_pk(
|
||||||
@@ -78,6 +92,10 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
|
|||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.admin_jobmarkforreexport(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
@@ -94,8 +112,26 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
|
|||||||
variables: { jobId: job.id, date_exported: moment() },
|
variables: { jobId: job.id, date_exported: moment() },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: job.id,
|
||||||
|
successful: true,
|
||||||
|
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.admin_jobmarkexported(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
|
|||||||
@@ -4,21 +4,29 @@ 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 { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {
|
import {
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
|
||||||
|
|
||||||
export function JobsAdminUnvoid({ bodyshop, job, currentUser }) {
|
export function JobsAdminUnvoid({
|
||||||
|
insertAuditTrail,
|
||||||
|
bodyshop,
|
||||||
|
job,
|
||||||
|
currentUser,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [updateJob] = useMutation(gql`
|
const [updateJob] = useMutation(gql`
|
||||||
@@ -84,6 +92,11 @@ mutation UNVOID_JOB($jobId: uuid!) {
|
|||||||
|
|
||||||
if (!result.errors) {
|
if (!result.errors) {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.admin_unvoicejob(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.saving", {
|
message: t("jobs.errors.saving", {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -27,6 +26,7 @@ export function JobsCloseExportButton({
|
|||||||
jobId,
|
jobId,
|
||||||
disabled,
|
disabled,
|
||||||
setSelectedJobs,
|
setSelectedJobs,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -46,13 +46,10 @@ export function JobsCloseExportButton({
|
|||||||
//Check if it's a QBO Setup.
|
//Check if it's a QBO Setup.
|
||||||
let PartnerResponse;
|
let PartnerResponse;
|
||||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(
|
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
||||||
`/qbo/receivables`,
|
jobIds: [jobId],
|
||||||
{
|
elgen: true,
|
||||||
jobIds: [jobId],
|
});
|
||||||
},
|
|
||||||
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
//Default is QBD
|
//Default is QBD
|
||||||
|
|
||||||
@@ -117,58 +114,64 @@ export function JobsCloseExportButton({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await insertExportLog({
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
variables: {
|
//QBO Logs are handled server side.
|
||||||
logs: [
|
await insertExportLog({
|
||||||
{
|
variables: {
|
||||||
bodyshopid: bodyshop.id,
|
logs: [
|
||||||
jobid: jobId,
|
{
|
||||||
successful: false,
|
bodyshopid: bodyshop.id,
|
||||||
message: JSON.stringify(
|
jobid: jobId,
|
||||||
failedTransactions.map((ft) => ft.errorMessage)
|
successful: false,
|
||||||
),
|
message: JSON.stringify(
|
||||||
useremail: currentUser.email,
|
failedTransactions.map((ft) => ft.errorMessage)
|
||||||
},
|
),
|
||||||
],
|
useremail: currentUser.email,
|
||||||
},
|
},
|
||||||
});
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
//Insert success export log.
|
//Insert success export log.
|
||||||
await insertExportLog({
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
variables: {
|
//QBO Logs are handled server side.
|
||||||
logs: [
|
await insertExportLog({
|
||||||
{
|
variables: {
|
||||||
bodyshopid: bodyshop.id,
|
logs: [
|
||||||
jobid: jobId,
|
{
|
||||||
successful: true,
|
bodyshopid: bodyshop.id,
|
||||||
useremail: currentUser.email,
|
jobid: jobId,
|
||||||
},
|
successful: true,
|
||||||
],
|
useremail: currentUser.email,
|
||||||
},
|
},
|
||||||
});
|
],
|
||||||
|
|
||||||
const jobUpdateResponse = await updateJob({
|
|
||||||
variables: {
|
|
||||||
jobId: jobId,
|
|
||||||
job: {
|
|
||||||
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
|
|
||||||
date_exported: new Date(),
|
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (!jobUpdateResponse.errors) {
|
const jobUpdateResponse = await updateJob({
|
||||||
notification.open({
|
variables: {
|
||||||
type: "success",
|
jobId: jobId,
|
||||||
key: "jobsuccessexport",
|
job: {
|
||||||
message: t("jobs.successes.exported"),
|
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
|
||||||
});
|
date_exported: new Date(),
|
||||||
} else {
|
},
|
||||||
notification["error"]({
|
},
|
||||||
message: t("jobs.errors.exporting", {
|
|
||||||
error: JSON.stringify(jobUpdateResponse.error),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!jobUpdateResponse.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "jobsuccessexport",
|
||||||
|
message: t("jobs.successes.exported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting", {
|
||||||
|
error: JSON.stringify(jobUpdateResponse.error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (setSelectedJobs) {
|
if (setSelectedJobs) {
|
||||||
setSelectedJobs((selectedJobs) => {
|
setSelectedJobs((selectedJobs) => {
|
||||||
@@ -176,7 +179,7 @@ export function JobsCloseExportButton({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,11 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel forceRender key="claim" header={t("menus.jobsdetail.claimdetail")}>
|
<Collapse.Panel
|
||||||
|
forceRender
|
||||||
|
key="claim"
|
||||||
|
header={t("menus.jobsdetail.claimdetail")}
|
||||||
|
>
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
||||||
<Input />
|
<Input />
|
||||||
@@ -193,7 +197,8 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel forceRender
|
<Collapse.Panel
|
||||||
|
forceRender
|
||||||
key="financial"
|
key="financial"
|
||||||
header={t("menus.jobsdetail.financials")}
|
header={t("menus.jobsdetail.financials")}
|
||||||
>
|
>
|
||||||
@@ -204,7 +209,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||||
<Select>
|
<Select allowClear>
|
||||||
<Select.Option value="W">
|
<Select.Option value="W">
|
||||||
{t("jobs.labels.deductible.waived")}
|
{t("jobs.labels.deductible.waived")}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
|
|||||||
@@ -16,15 +16,18 @@ export function JobsDetailChangeFilehandler({ disabled, form, bodyshop }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<div>
|
<Menu
|
||||||
<Menu onClick={handleClick}>
|
onClick={handleClick}
|
||||||
{bodyshop.md_filehandlers.map((est, idx) => (
|
style={{
|
||||||
<Menu.Item value={est} key={idx}>
|
columnCount: Math.floor(bodyshop.md_filehandlers.length / 10) + 1,
|
||||||
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
|
}}
|
||||||
</Menu.Item>
|
>
|
||||||
))}
|
{bodyshop.md_filehandlers.map((est, idx) => (
|
||||||
</Menu>
|
<Menu.Item value={est} key={idx} style={{ breakInside: "avoid" }}>
|
||||||
</div>
|
{`${est.ins_ct_fn} ${est.ins_ct_ln}`}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -216,6 +216,22 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
|||||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||||
<JobsRelatedRos jobid={job.id} job={job} />
|
<JobsRelatedRos jobid={job.id} job={job} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
|
{job.vehicle && job.vehicle.notes && (
|
||||||
|
<DataLabel label={t("vehicles.fields.notes")}>
|
||||||
|
<span style={{ whiteSpace: "pre" }}>{job.vehicle.notes}</span>
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
// job.vehicle && job.vehicle.v_paint_codes && (
|
||||||
|
// <DataLabel label={t("vehicles.fields.v_paint_codes")}>
|
||||||
|
// <span style={{ whiteSpace: "pre" }}>
|
||||||
|
// {Object.keys(job.vehicle.v_paint_codes).map((key, idx) => (
|
||||||
|
// <Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||||
|
// ))}
|
||||||
|
// </span>
|
||||||
|
// </DataLabel>
|
||||||
|
// )
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
nostyle
|
noStyle
|
||||||
shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}
|
shouldUpdate={(prev, cur) => prev.auto_add_ats !== cur.auto_add_ats}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons";
|
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
|
||||||
import { Card, Col, Row, Space, Button } from "antd";
|
import { Button, Card, Col, Row, Space } 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";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ function JobsDocumentGalleryExternal({
|
|||||||
id: value.id,
|
id: value.id,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
tags: [{ value: value.type, title: value.type }],
|
tags: [{ value: value.type, title: value.type }],
|
||||||
|
size: value.size,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Card, Space } from "antd";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Gallery from "react-grid-gallery";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
getBillMedia,
|
||||||
|
getJobMedia,
|
||||||
|
toggleMediaSelected,
|
||||||
|
} from "../../redux/media/media.actions";
|
||||||
|
import { selectAllMedia } from "../../redux/media/media.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
|
||||||
|
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
|
||||||
|
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
|
||||||
|
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
allMedia: selectAllMedia,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
getJobMedia: (id) => dispatch(getJobMedia(id)),
|
||||||
|
getBillMedia: ({ jobid, invoice_number }) => {
|
||||||
|
dispatch(getBillMedia({ jobid, invoice_number }));
|
||||||
|
},
|
||||||
|
toggleMediaSelected: ({ jobid, filename }) =>
|
||||||
|
dispatch(toggleMediaSelected({ jobid, filename })),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobsDocumentsLocalGallery);
|
||||||
|
|
||||||
|
export function JobsDocumentsLocalGallery({
|
||||||
|
bodyshop,
|
||||||
|
toggleMediaSelected,
|
||||||
|
getJobMedia,
|
||||||
|
getBillMedia,
|
||||||
|
allMedia,
|
||||||
|
job,
|
||||||
|
invoice_number,
|
||||||
|
vendorid,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
useEffect(() => {
|
||||||
|
if (job) {
|
||||||
|
if (invoice_number) {
|
||||||
|
getBillMedia({ jobid: job.id, invoice_number });
|
||||||
|
} else {
|
||||||
|
getJobMedia(job.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [job, invoice_number, getJobMedia, getBillMedia]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (job) {
|
||||||
|
if (invoice_number) {
|
||||||
|
getBillMedia({ jobid: job.id, invoice_number });
|
||||||
|
} else {
|
||||||
|
getJobMedia(job.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
|
||||||
|
<Button>{t("documents.labels.openinexplorer")}</Button>
|
||||||
|
</a>
|
||||||
|
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
|
||||||
|
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
|
||||||
|
</Space>
|
||||||
|
<Card>
|
||||||
|
<DocumentsLocalUploadComponent
|
||||||
|
job={job}
|
||||||
|
invoice_number={invoice_number}
|
||||||
|
vendorid={vendorid}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card title={t("jobs.labels.documents-images")}>
|
||||||
|
<Gallery
|
||||||
|
images={(allMedia && allMedia[job.id]) || []}
|
||||||
|
backdropClosesModal={true}
|
||||||
|
onSelectImage={(index, image) => {
|
||||||
|
toggleMediaSelected({ jobid: job.id, filename: image.filename });
|
||||||
|
}}
|
||||||
|
onClickImage={(props) => {
|
||||||
|
window.open(
|
||||||
|
props.target.src,
|
||||||
|
"_blank",
|
||||||
|
"toolbar=0,location=0,menubar=0"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Button, Form, Popover, Space } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { getJobMedia } from "../../redux/media/media.actions";
|
||||||
|
import { selectAllMedia } from "../../redux/media/media.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import cleanAxios from "../../utils/CleanAxios";
|
||||||
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
allMedia: selectAllMedia,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
getJobMedia: (id) => dispatch(getJobMedia(id)),
|
||||||
|
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobsDocumentsLocalGalleryReassign);
|
||||||
|
|
||||||
|
export function JobsDocumentsLocalGalleryReassign({
|
||||||
|
bodyshop,
|
||||||
|
jobid,
|
||||||
|
allMedia,
|
||||||
|
getJobMedia,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleFinish = async ({ jobid: newJobid }) => {
|
||||||
|
setLoading(true);
|
||||||
|
const selectedDocuments = allMedia[jobid].filter((m) => m.isSelected);
|
||||||
|
|
||||||
|
await cleanAxios.post(
|
||||||
|
`${bodyshop.localmediaserverhttp}/jobs/move`,
|
||||||
|
{
|
||||||
|
from_jobid: jobid,
|
||||||
|
jobid: newJobid,
|
||||||
|
files: selectedDocuments.map((f) => f.filename),
|
||||||
|
},
|
||||||
|
{ headers: { ims_token: bodyshop.localmediatoken } }
|
||||||
|
);
|
||||||
|
|
||||||
|
getJobMedia(jobid);
|
||||||
|
setVisible(false);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const popContent = (
|
||||||
|
<div>
|
||||||
|
<Form onFinish={handleFinish} layout="vertical" form={form}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("documents.labels.newjobid")}
|
||||||
|
style={{ width: "20rem" }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
name={"jobid"}
|
||||||
|
>
|
||||||
|
<JobSearchSelect />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={() => form.submit()}>
|
||||||
|
{t("general.actions.submit")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setVisible(false)}>
|
||||||
|
{t("general.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover content={popContent} visible={visible}>
|
||||||
|
<Button
|
||||||
|
//disabled={selectedImages.length < 1}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{t("documents.actions.reassign")}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Button, Space } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
selectAllmediaForJob,
|
||||||
|
deselectAllMediaForJob,
|
||||||
|
} from "../../redux/media/media.actions";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
selectAllmediaForJob: (jobid) => dispatch(selectAllmediaForJob(jobid)),
|
||||||
|
deselectAllmediaForJob: (jobid) => dispatch(deselectAllMediaForJob(jobid)),
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobsDocumentsLocalGallerySelectAllComponent);
|
||||||
|
|
||||||
|
export function JobsDocumentsLocalGallerySelectAllComponent({
|
||||||
|
jobid,
|
||||||
|
selectAllmediaForJob,
|
||||||
|
deselectAllmediaForJob,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// onSelectImage={(index, image) => {
|
||||||
|
// toggleMediaSelected({ jobid: job.id, filename: image.filename });
|
||||||
|
// }}
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
selectAllmediaForJob({ jobid });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeselectAll = () => {
|
||||||
|
deselectAllmediaForJob({ jobid });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<Button onClick={handleSelectAll}>
|
||||||
|
{t("general.actions.selectall")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={handleDeselectAll}>
|
||||||
|
{t("general.actions.deselectall")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export function JobsExportAllButton({
|
|||||||
disabled,
|
disabled,
|
||||||
loadingCallback,
|
loadingCallback,
|
||||||
completedCallback,
|
completedCallback,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateJob] = useMutation(UPDATE_JOBS);
|
const [updateJob] = useMutation(UPDATE_JOBS);
|
||||||
@@ -39,6 +40,7 @@ export function JobsExportAllButton({
|
|||||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
||||||
jobIds: jobIds,
|
jobIds: jobIds,
|
||||||
|
elgen: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let QbXmlResponse;
|
let QbXmlResponse;
|
||||||
@@ -83,6 +85,7 @@ export function JobsExportAllButton({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("PartnerResponse", PartnerResponse);
|
console.log("PartnerResponse", PartnerResponse);
|
||||||
const groupedData = _.groupBy(
|
const groupedData = _.groupBy(
|
||||||
PartnerResponse.data,
|
PartnerResponse.data,
|
||||||
@@ -106,61 +109,70 @@ export function JobsExportAllButton({
|
|||||||
});
|
});
|
||||||
//Call is not awaited as it is not critical to finish before proceeding.
|
//Call is not awaited as it is not critical to finish before proceeding.
|
||||||
});
|
});
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
jobid: key,
|
|
||||||
successful: false,
|
|
||||||
message: JSON.stringify(
|
|
||||||
failedTransactions.map((ft) => ft.errorMessage)
|
|
||||||
),
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
jobid: key,
|
|
||||||
successful: true,
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const jobUpdateResponse = await updateJob({
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
variables: {
|
//QBO Logs are handled server side.
|
||||||
jobIds: [key],
|
await insertExportLog({
|
||||||
fields: {
|
variables: {
|
||||||
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
|
logs: [
|
||||||
date_exported: new Date(),
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: key,
|
||||||
|
successful: false,
|
||||||
|
message: JSON.stringify(
|
||||||
|
failedTransactions.map((ft) => ft.errorMessage)
|
||||||
|
),
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
}
|
||||||
|
} else {
|
||||||
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
|
//QBO Logs are handled server side.
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: key,
|
||||||
|
successful: true,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!jobUpdateResponse.errors) {
|
const jobUpdateResponse = await updateJob({
|
||||||
notification.open({
|
variables: {
|
||||||
type: "success",
|
jobIds: [key],
|
||||||
key: "jobsuccessexport",
|
fields: {
|
||||||
message: t("jobs.successes.exported"),
|
status:
|
||||||
});
|
bodyshop.md_ro_statuses.default_exported || "Exported*",
|
||||||
} else {
|
date_exported: new Date(),
|
||||||
notification["error"]({
|
},
|
||||||
message: t("jobs.errors.exporting", {
|
},
|
||||||
error: JSON.stringify(jobUpdateResponse.error),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!jobUpdateResponse.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "jobsuccessexport",
|
||||||
|
message: t("jobs.successes.exported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting", {
|
||||||
|
error: JSON.stringify(jobUpdateResponse.error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||||
|
|
||||||
if (!!completedCallback) completedCallback([]);
|
if (!!completedCallback) completedCallback([]);
|
||||||
if (!!loadingCallback) loadingCallback(false);
|
if (!!loadingCallback) loadingCallback(false);
|
||||||
|
|||||||
@@ -137,9 +137,9 @@ export function JobsList({ bodyshop }) {
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.owner ? (
|
return record.ownerid ? (
|
||||||
<Link
|
<Link
|
||||||
to={"/manage/owners/" + record.owner.id}
|
to={"/manage/owners/" + record.ownerid}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<OwnerNameDisplay ownerObject={record} />
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
|
|||||||
jobId={jobId}
|
jobId={jobId}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk.notes : null}
|
data={data ? data.jobs_by_pk.notes : null}
|
||||||
|
relatedRos={
|
||||||
|
data ? data.jobs_by_pk.vehicle && data.jobs_by_pk.vehicle.jobs : null
|
||||||
|
}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
deleteLoading={deleteLoading}
|
deleteLoading={deleteLoading}
|
||||||
handleNoteDelete={handleNoteDelete}
|
handleNoteDelete={handleNoteDelete}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function JobNotesComponent({
|
|||||||
setNoteUpsertContext,
|
setNoteUpsertContext,
|
||||||
deleteLoading,
|
deleteLoading,
|
||||||
ro_number,
|
ro_number,
|
||||||
|
relatedRos,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Templates = TemplateList("job_special", {
|
const Templates = TemplateList("job_special", {
|
||||||
@@ -149,6 +150,7 @@ export function JobNotesComponent({
|
|||||||
actions: { refetch: refetch },
|
actions: { refetch: refetch },
|
||||||
context: {
|
context: {
|
||||||
jobId: jobId,
|
jobId: jobId,
|
||||||
|
relatedRos: relatedRos,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,355 @@
|
|||||||
|
import {
|
||||||
|
ExclamationCircleFilled,
|
||||||
|
PauseCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Button, Card, Grid, Input, Space, Table } from "antd";
|
||||||
|
import queryString from "query-string";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { onlyUnique } from "../../utils/arrayHelper";
|
||||||
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JobsReadyList({ bodyshop }) {
|
||||||
|
const searchParams = queryString.parse(useLocation().search);
|
||||||
|
const { selected } = searchParams;
|
||||||
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
|
.filter((screen) => !!screen[1])
|
||||||
|
.slice(-1)[0];
|
||||||
|
|
||||||
|
const readyStatuses = useMemo(() => {
|
||||||
|
if (bodyshop.md_ro_statuses.ready_statuses)
|
||||||
|
return bodyshop.md_ro_statuses.ready_statuses;
|
||||||
|
|
||||||
|
return bodyshop.md_ro_statuses.post_production_statuses.filter(
|
||||||
|
(s) =>
|
||||||
|
s !== bodyshop.md_ro_statuses.default_invoiced &&
|
||||||
|
s !== bodyshop.md_ro_statuses.default_exported
|
||||||
|
);
|
||||||
|
}, [bodyshop.md_ro_statuses]);
|
||||||
|
|
||||||
|
const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||||
|
variables: {
|
||||||
|
statuses: readyStatuses,
|
||||||
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
sortedInfo: {},
|
||||||
|
filteredInfo: { text: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const history = useHistory();
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
|
const jobs = data
|
||||||
|
? searchText === ""
|
||||||
|
? data.jobs
|
||||||
|
: data.jobs.filter(
|
||||||
|
(j) =>
|
||||||
|
(j.ro_number || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.ownr_co_nm || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.comments || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.ownr_fn || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.ownr_ln || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
|
||||||
|
(j.plate_no || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.v_model_desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(j.v_make_desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnRowClick = (record) => {
|
||||||
|
if (record) {
|
||||||
|
if (record.id) {
|
||||||
|
history.push({
|
||||||
|
search: queryString.stringify({
|
||||||
|
...searchParams,
|
||||||
|
selected: record.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ro_number"),
|
||||||
|
dataIndex: "ro_number",
|
||||||
|
key: "ro_number",
|
||||||
|
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<Link
|
||||||
|
to={"/manage/jobs/" + record.id}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
{record.ro_number || t("general.labels.na")}
|
||||||
|
{record.production_vars && record.production_vars.alert ? (
|
||||||
|
<ExclamationCircleFilled className="production-alert" />
|
||||||
|
) : null}
|
||||||
|
{record.suspended && (
|
||||||
|
<PauseCircleOutlined style={{ color: "orangered" }} />
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
responsive: ["md"],
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.owner ? (
|
||||||
|
<Link
|
||||||
|
to={"/manage/owners/" + record.owner.id}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<OwnerNameDisplay ownerObject={record} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ownr_ph1"),
|
||||||
|
dataIndex: "ownr_ph1",
|
||||||
|
key: "ownr_ph1",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
render: (text, record) => (
|
||||||
|
<ChatOpenButton phone={record.ownr_ph1} jobid={record.id} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ownr_ph2"),
|
||||||
|
dataIndex: "ownr_ph2",
|
||||||
|
key: "ownr_ph2",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
render: (text, record) => (
|
||||||
|
<ChatOpenButton phone={record.ownr_ph2} jobid={record.id} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.status"),
|
||||||
|
dataIndex: "status",
|
||||||
|
key: "status",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
|
filters:
|
||||||
|
(jobs &&
|
||||||
|
jobs
|
||||||
|
.map((j) => j.status)
|
||||||
|
.filter(onlyUnique)
|
||||||
|
.map((s) => {
|
||||||
|
return {
|
||||||
|
text: s || "No Status*",
|
||||||
|
value: [s],
|
||||||
|
};
|
||||||
|
})) ||
|
||||||
|
[],
|
||||||
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) => {
|
||||||
|
return record.vehicleid ? (
|
||||||
|
<Link
|
||||||
|
to={"/manage/vehicles/" + record.vehicleid}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||||
|
record.v_model_desc || ""
|
||||||
|
}`}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${
|
||||||
|
record.v_model_desc || ""
|
||||||
|
}`}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.plate_no"),
|
||||||
|
dataIndex: "plate_no",
|
||||||
|
key: "plate_no",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
responsive: ["md"],
|
||||||
|
sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.clm_no"),
|
||||||
|
dataIndex: "clm_no",
|
||||||
|
key: "clm_no",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
|
||||||
|
render: (text, record) =>
|
||||||
|
`${record.clm_no || ""}${
|
||||||
|
record.po_number ? ` (PO: ${record.po_number})` : ""
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.ins_co_nm"),
|
||||||
|
dataIndex: "ins_co_nm",
|
||||||
|
key: "ins_co_nm",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.clm_total"),
|
||||||
|
dataIndex: "clm_total",
|
||||||
|
key: "clm_total",
|
||||||
|
responsive: ["md"],
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
sorter: (a, b) => a.clm_total - b.clm_total,
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => (
|
||||||
|
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.comment"),
|
||||||
|
dataIndex: "comment",
|
||||||
|
key: "comment",
|
||||||
|
ellipsis: true,
|
||||||
|
responsive: ["md"],
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: t("jobs.fields.owner_owing"),
|
||||||
|
// dataIndex: "owner_owing",
|
||||||
|
// key: "owner_owing",
|
||||||
|
// responsive: ["md"],
|
||||||
|
// render: (text, record) => (
|
||||||
|
// <CurrencyFormatter>{record.owner_owing}</CurrencyFormatter>
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
|
||||||
|
const scrollMapper = {
|
||||||
|
xs: true,
|
||||||
|
sm: true,
|
||||||
|
md: true,
|
||||||
|
lg: "100%",
|
||||||
|
xl: "100%",
|
||||||
|
xxl: "100%",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={t("titles.bc.jobs-ready")}
|
||||||
|
extra={
|
||||||
|
<Space wrap>
|
||||||
|
<span>({readyStatuses && readyStatuses.join(", ")})</span>
|
||||||
|
<Button onClick={() => refetch()}>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
}}
|
||||||
|
value={searchText}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
pagination={{ defaultPageSize: 50 }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={jobs}
|
||||||
|
scroll={{
|
||||||
|
x: selectedBreakpoint ? scrollMapper[selectedBreakpoint[0]] : "100%",
|
||||||
|
}}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (record) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
},
|
||||||
|
selectedRowKeys: [selected],
|
||||||
|
type: "radio",
|
||||||
|
}}
|
||||||
|
onChange={handleTableChange}
|
||||||
|
onRow={(record, rowIndex) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => {
|
||||||
|
handleOnRowClick(record);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(JobsReadyList);
|
||||||
@@ -1,51 +1,92 @@
|
|||||||
import { Col, Form, Input, Row, Switch } from "antd";
|
import { Checkbox, Col, Form, Input, Row, Space, Switch, Tag } 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 { createStructuredSelector } from "reselect";
|
||||||
|
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
||||||
import NotesPresetButton from "../notes-preset-button/notes-preset-button.component";
|
import NotesPresetButton from "../notes-preset-button/notes-preset-button.component";
|
||||||
|
|
||||||
export default function NoteUpsertModalComponent({ form }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
noteUpsertModal: selectNoteUpsert,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(NoteUpsertModalComponent);
|
||||||
|
|
||||||
|
export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { jobId, existingNote, relatedRos } = noteUpsertModal.context;
|
||||||
|
|
||||||
|
const filteredRelatedRos = relatedRos
|
||||||
|
? relatedRos.filter((j) => j.id !== jobId)
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={[16, 16]}>
|
<>
|
||||||
<Col span={8}>
|
<Row gutter={[16, 16]}>
|
||||||
<Form.Item
|
<Col span={8}>
|
||||||
label={t("notes.fields.critical")}
|
<Form.Item
|
||||||
name="critical"
|
label={t("notes.fields.critical")}
|
||||||
valuePropName="checked"
|
name="critical"
|
||||||
>
|
valuePropName="checked"
|
||||||
<Switch />
|
>
|
||||||
</Form.Item>
|
<Switch />
|
||||||
</Col>
|
</Form.Item>
|
||||||
<Col span={8}>
|
</Col>
|
||||||
<Form.Item
|
<Col span={8}>
|
||||||
label={t("notes.fields.private")}
|
<Form.Item
|
||||||
name="private"
|
label={t("notes.fields.private")}
|
||||||
valuePropName="checked"
|
name="private"
|
||||||
>
|
valuePropName="checked"
|
||||||
<Switch />
|
>
|
||||||
</Form.Item>
|
<Switch />
|
||||||
</Col>
|
</Form.Item>
|
||||||
<Col span={8}>
|
</Col>
|
||||||
<NotesPresetButton form={form} />
|
<Col span={8}>
|
||||||
</Col>
|
<NotesPresetButton form={form} />
|
||||||
<Col span={24}>
|
</Col>
|
||||||
<Form.Item
|
<Col span={24}>
|
||||||
label={t("notes.fields.text")}
|
<Form.Item
|
||||||
name="text"
|
label={t("notes.fields.text")}
|
||||||
rules={[
|
name="text"
|
||||||
{
|
rules={[
|
||||||
required: true,
|
{
|
||||||
//message: t("general.validation.required"),
|
required: true,
|
||||||
},
|
//message: t("general.validation.required"),
|
||||||
]}
|
},
|
||||||
>
|
]}
|
||||||
<Input.TextArea
|
>
|
||||||
rows={8}
|
<Input.TextArea
|
||||||
placeholder={t("notes.labels.newnoteplaceholder")}
|
rows={8}
|
||||||
/>
|
placeholder={t("notes.labels.newnoteplaceholder")}
|
||||||
</Form.Item>
|
/>
|
||||||
</Col>
|
</Form.Item>
|
||||||
</Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div>
|
||||||
|
<div>{!existingNote && t("notes.labels.addtorelatedro")}</div>
|
||||||
|
{!existingNote &&
|
||||||
|
filteredRelatedRos.map((j, idx) => (
|
||||||
|
<Space key={j.id} align="center">
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
name={["relatedros", j.id]}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
<Tag>
|
||||||
|
{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
|
||||||
|
j.status ? ` | ${j.status}` : ""
|
||||||
|
}`}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import React, { useEffect } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
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 { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -48,7 +48,9 @@ export function NoteUpsertModalContainer({
|
|||||||
}
|
}
|
||||||
}, [existingNote, form, visible]);
|
}, [existingNote, form, visible]);
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = async (formValues) => {
|
||||||
|
const { relatedros, ...values } = formValues;
|
||||||
|
|
||||||
if (existingNote) {
|
if (existingNote) {
|
||||||
logImEXEvent("job_note_update");
|
logImEXEvent("job_note_update");
|
||||||
|
|
||||||
@@ -70,24 +72,44 @@ export function NoteUpsertModalContainer({
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
} else {
|
} else {
|
||||||
logImEXEvent("job_note_insert");
|
logImEXEvent("job_note_insert");
|
||||||
|
const AdditionalNoteInserts = relatedros
|
||||||
|
? Object.keys(relatedros).filter((key) => relatedros[key])
|
||||||
|
: [];
|
||||||
|
|
||||||
insertNote({
|
await insertNote({
|
||||||
variables: {
|
variables: {
|
||||||
noteInput: [
|
noteInput: [
|
||||||
{ ...values, jobid: jobId, created_by: currentUser.email },
|
{ ...values, jobid: jobId, created_by: currentUser.email },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}).then((r) => {
|
});
|
||||||
if (refetch) refetch();
|
|
||||||
form.resetFields();
|
if (AdditionalNoteInserts.length > 0) {
|
||||||
toggleModalVisible();
|
//Insert the others.
|
||||||
notification["success"]({
|
AdditionalNoteInserts.forEach(async (newJobId) => {
|
||||||
message: t("notes.successes.create"),
|
await insertNote({
|
||||||
});
|
variables: {
|
||||||
insertAuditTrail({
|
noteInput: [
|
||||||
jobid: context.jobId,
|
{ ...values, jobid: newJobId, created_by: currentUser.email },
|
||||||
operation: AuditTrailMapping.jobnoteadded(),
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: newJobId,
|
||||||
|
operation: AuditTrailMapping.jobnoteadded(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refetch) refetch();
|
||||||
|
form.resetFields();
|
||||||
|
toggleModalVisible();
|
||||||
|
notification["success"]({
|
||||||
|
message: t("notes.successes.create"),
|
||||||
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: context.jobId,
|
||||||
|
operation: AuditTrailMapping.jobnoteadded(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,9 +22,17 @@ export function NotesPresetButton({ bodyshop, form }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu>
|
<Menu
|
||||||
|
style={{
|
||||||
|
columnCount: Math.floor(bodyshop.md_notes_presets.length / 10) + 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{bodyshop.md_notes_presets.map((i, idx) => (
|
{bodyshop.md_notes_presets.map((i, idx) => (
|
||||||
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
|
<Menu.Item
|
||||||
|
onClick={() => handleSelect(i)}
|
||||||
|
key={idx}
|
||||||
|
style={{ breakInside: "avoid" }}
|
||||||
|
>
|
||||||
{i.label}
|
{i.label}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormFieldsChanged form={form} />
|
<FormFieldsChanged form={form} />
|
||||||
|
|
||||||
<LayoutFormRow header={t("owners.forms.name")}>
|
<LayoutFormRow header={t("owners.forms.name")}>
|
||||||
<Form.Item label={t("owners.fields.ownr_title")} name="ownr_title">
|
<Form.Item label={t("owners.fields.ownr_title")} name="ownr_title">
|
||||||
<Input />
|
<Input />
|
||||||
@@ -29,7 +28,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow header={t("owners.forms.address")}>
|
<LayoutFormRow header={t("owners.forms.address")}>
|
||||||
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
|
<Form.Item label={t("owners.fields.ownr_addr1")} name="ownr_addr1">
|
||||||
<Input />
|
<Input />
|
||||||
@@ -50,7 +48,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
|
||||||
<LayoutFormRow header={t("owners.forms.contact")}>
|
<LayoutFormRow header={t("owners.forms.contact")}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("owners.fields.allow_text_message")}
|
label={t("owners.fields.allow_text_message")}
|
||||||
@@ -98,6 +95,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
|
<Form.Item label={t("owners.fields.note")} name="note">
|
||||||
|
<Input.TextArea rows={4} />
|
||||||
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ export default function OwnerFindModalComponent({
|
|||||||
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
|
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("owners.fields.note"),
|
||||||
|
dataIndex: "note",
|
||||||
|
key: "note",
|
||||||
|
render: (text, record) => (
|
||||||
|
<span style={{ whiteSpace: "pre" }}>{record.note}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleOnRowClick = (record) => {
|
const handleOnRowClick = (record) => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function OwnerFindModalContainer({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalProps.visible && owner) {
|
if (modalProps.visible && owner) {
|
||||||
const s = OwnerNameDisplayFunction(owner);
|
const s = OwnerNameDisplayFunction(owner, true);
|
||||||
|
|
||||||
setSearchText(s.trim());
|
setSearchText(s.trim());
|
||||||
callSearchowners({ variables: { search: s.trim() } });
|
callSearchowners({ variables: { search: s.trim() } });
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function OwnerNameDisplay({ bodyshop, ownerObject }) {
|
|||||||
}`.trim();
|
}`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OwnerNameDisplayFunction(ownerObject) {
|
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
|
||||||
const emptyTest =
|
const emptyTest =
|
||||||
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
|
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export function OwnerNameDisplayFunction(ownerObject) {
|
|||||||
|
|
||||||
const rdxStore = store.getState();
|
const rdxStore = store.getState();
|
||||||
|
|
||||||
if (rdxStore.user.bodyshop.last_name_first)
|
if (rdxStore.user.bodyshop.last_name_first && !forceFirstLast)
|
||||||
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
|
return `${ownerObject.ownr_ln || ""}, ${ownerObject.ownr_fn || ""} ${
|
||||||
ownerObject.ownr_co_nm || ""
|
ownerObject.ownr_co_nm || ""
|
||||||
}`.trim();
|
}`.trim();
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Checkbox, notification, Space, Spin } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { MUTATION_UPDATE_PO_CM_REECEIVED } from "../../graphql/parts-orders.queries";
|
||||||
|
|
||||||
|
export default function PartsOrderCmReceived({
|
||||||
|
checked,
|
||||||
|
orderLineId,
|
||||||
|
partsOrderId,
|
||||||
|
}) {
|
||||||
|
const [updateLine] = useMutation(MUTATION_UPDATE_PO_CM_REECEIVED);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = async (e) => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await updateLine({
|
||||||
|
variables: {
|
||||||
|
partsLineId: orderLineId,
|
||||||
|
partsOrder: { cm_received: e.target.checked },
|
||||||
|
},
|
||||||
|
update(cache) {
|
||||||
|
cache.modify({
|
||||||
|
id: cache.identify({
|
||||||
|
id: partsOrderId,
|
||||||
|
__typename: "parts_orders",
|
||||||
|
}),
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
parts_order_lines(ex, { readField }) {
|
||||||
|
console.log(ex);
|
||||||
|
return ex.map((lineref) => {
|
||||||
|
if (orderLineId.id !== readField("id", lineref)) {
|
||||||
|
lineref.cm_received = e.target.checked;
|
||||||
|
}
|
||||||
|
return lineref;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!!result.errors) {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("parts_orders.successes.line_updated"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("parts_orders.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<Checkbox checked={checked} onChange={handleChange} />
|
||||||
|
{loading && <Spin size="small" />}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Popconfirm } from "antd";
|
||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DELETE_PARTS_ORDER_LINE } from "../../graphql/parts-orders.queries";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
|
||||||
|
export default function PartsOrderDeleteLine({
|
||||||
|
disabled,
|
||||||
|
partsLineId,
|
||||||
|
partsOrderId,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [deletePartsOrderLine] = useMutation(DELETE_PARTS_ORDER_LINE);
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
title={t("parts_orders.labels.confirmdelete")}
|
||||||
|
disabled={disabled}
|
||||||
|
onConfirm={async () => {
|
||||||
|
//Delete the parts return.!
|
||||||
|
|
||||||
|
await deletePartsOrderLine({
|
||||||
|
variables: { partsOrderLineId: partsLineId },
|
||||||
|
update(cache) {
|
||||||
|
cache.modify({
|
||||||
|
id: cache.identify({
|
||||||
|
__typename: "parts_orders",
|
||||||
|
id: partsOrderId,
|
||||||
|
}),
|
||||||
|
fields: {
|
||||||
|
parts_order_lines(cached, { readField }) {
|
||||||
|
return cached.filter((c) => {
|
||||||
|
return readField("id", c) !== partsLineId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button disabled={disabled}>
|
||||||
|
<DeleteFilled />
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ import { alphaSort } 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 PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||||
|
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||||
|
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||||
@@ -77,6 +79,7 @@ export function PartsOrderListTableComponent({
|
|||||||
});
|
});
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const selectedpartsorder = search.partsorderid;
|
const selectedpartsorder = search.partsorderid;
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||||
|
|
||||||
@@ -346,6 +349,23 @@ export function PartsOrderListTableComponent({
|
|||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t("parts_orders.fields.cm_received"),
|
||||||
|
dataIndex: "cm_received",
|
||||||
|
key: "cm_received",
|
||||||
|
render: (text, record) => (
|
||||||
|
<PartsOrderCmReceived
|
||||||
|
orderLineId={record.id}
|
||||||
|
checked={record.cm_received}
|
||||||
|
partsorderid={selectedPartsOrderRecord.id}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
title: t("parts_orders.fields.backordered_on"),
|
title: t("parts_orders.fields.backordered_on"),
|
||||||
dataIndex: "backordered_on",
|
dataIndex: "backordered_on",
|
||||||
@@ -372,12 +392,21 @@ export function PartsOrderListTableComponent({
|
|||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<PartsOrderLineBackorderButton
|
<Space wrap>
|
||||||
disabled={jobRO}
|
<PartsOrderDeleteLine
|
||||||
partsOrderStatus={record.status}
|
disabled={jobRO}
|
||||||
partsLineId={record.id}
|
partsOrderStatus={record.status}
|
||||||
jobLineId={record.job_line_id}
|
partsLineId={record.id}
|
||||||
/>
|
partsOrderId={selectedpartsorder}
|
||||||
|
jobLineId={record.job_line_id}
|
||||||
|
/>
|
||||||
|
<PartsOrderLineBackorderButton
|
||||||
|
disabled={jobRO}
|
||||||
|
partsOrderStatus={record.status}
|
||||||
|
partsLineId={record.id}
|
||||||
|
jobLineId={record.job_line_id}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -403,6 +432,21 @@ export function PartsOrderListTableComponent({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredPartsOrders = parts_orders
|
||||||
|
? searchText === ""
|
||||||
|
? parts_orders
|
||||||
|
: parts_orders.filter(
|
||||||
|
(b) =>
|
||||||
|
(b.order_number || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(b.vendor.name || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t("parts_orders.labels.parts_orders")}
|
title={t("parts_orders.labels.parts_orders")}
|
||||||
@@ -413,8 +457,10 @@ export function PartsOrderListTableComponent({
|
|||||||
</Button>
|
</Button>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
value={searchText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setSearchText(e.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -438,7 +484,7 @@ export function PartsOrderListTableComponent({
|
|||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={parts_orders}
|
dataSource={filteredPartsOrders}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Menu,
|
Menu,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
Checkbox,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -114,6 +115,15 @@ export function PartsOrderModalComponent({
|
|||||||
</Space>
|
</Space>
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
|
{!isReturn && (
|
||||||
|
<Form.Item
|
||||||
|
name="removefrompartsqueue"
|
||||||
|
label={t("parts_orders.labels.removefrompartsqueue")}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">
|
<Divider orientation="left">
|
||||||
{t("parts_orders.labels.inthisorder")}
|
{t("parts_orders.labels.inthisorder")}
|
||||||
@@ -280,6 +290,7 @@ export function PartsOrderModalComponent({
|
|||||||
>
|
>
|
||||||
<Input.TextArea rows={3} />
|
<Input.TextArea rows={3} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Radio.Group
|
<Radio.Group
|
||||||
defaultValue={sendType}
|
defaultValue={sendType}
|
||||||
onChange={(e) => setSendType(e.target.value)}
|
onChange={(e) => setSendType(e.target.value)}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import PartsOrderModalComponent from "./parts-order-modal.component";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -90,8 +91,9 @@ export function PartsOrderModalContainer({
|
|||||||
|
|
||||||
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
|
const [insertPartOrder] = useMutation(INSERT_NEW_PARTS_ORDERS);
|
||||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
||||||
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async ({ removefrompartsqueue, ...values }) => {
|
||||||
logImEXEvent("parts_order_insert");
|
logImEXEvent("parts_order_insert");
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const insertResult = await insertPartOrder({
|
const insertResult = await insertPartOrder({
|
||||||
@@ -99,6 +101,7 @@ export function PartsOrderModalContainer({
|
|||||||
po: [
|
po: [
|
||||||
{
|
{
|
||||||
...values,
|
...values,
|
||||||
|
order_date: moment().format("YYYY-MM-DD"),
|
||||||
orderedby: currentUser.email,
|
orderedby: currentUser.email,
|
||||||
jobid: jobId,
|
jobid: jobId,
|
||||||
user_email: currentUser.email,
|
user_email: currentUser.email,
|
||||||
@@ -128,6 +131,17 @@ export function PartsOrderModalContainer({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!isReturn && removefrompartsqueue) {
|
||||||
|
await updateJob({
|
||||||
|
variables: {
|
||||||
|
jobId: jobId,
|
||||||
|
job: {
|
||||||
|
queued_for_parts: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: jobId,
|
jobid: jobId,
|
||||||
operation: isReturn
|
operation: isReturn
|
||||||
@@ -305,6 +319,7 @@ export function PartsOrderModalContainer({
|
|||||||
quantity: value.part_qty,
|
quantity: value.part_qty,
|
||||||
job_line_id: isReturn ? value.joblineid : value.id,
|
job_line_id: isReturn ? value.joblineid : value.id,
|
||||||
part_type: value.part_type,
|
part_type: value.part_type,
|
||||||
|
...(isReturn && { cm_received: false }),
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Form, Modal, notification } from "antd";
|
import { Form, Modal, notification } from "antd";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -31,7 +31,7 @@ export function PartsReceiveModalContainer({
|
|||||||
bodyshop,
|
bodyshop,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const { visible, context, actions } = partsReceiveModal;
|
const { visible, context, actions } = partsReceiveModal;
|
||||||
const { partsorderlines } = context;
|
const { partsorderlines } = context;
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export function PartsReceiveModalContainer({
|
|||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("parts_order_receive");
|
logImEXEvent("parts_order_receive");
|
||||||
|
setLoading(true);
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
values.partsorderlines.map((li) => {
|
values.partsorderlines.map((li) => {
|
||||||
return receivePartsLine({
|
return receivePartsLine({
|
||||||
@@ -75,7 +75,7 @@ export function PartsReceiveModalContainer({
|
|||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("parts_orders.successes.received"),
|
message: t("parts_orders.successes.received"),
|
||||||
});
|
});
|
||||||
|
setLoading(false);
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
};
|
};
|
||||||
@@ -96,6 +96,7 @@ export function PartsReceiveModalContainer({
|
|||||||
title={t("parts_orders.labels.receive")}
|
title={t("parts_orders.labels.receive")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
|
okButtonProps={{ loading: loading }}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
forceRender
|
forceRender
|
||||||
width="50%"
|
width="50%"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function PayableExportAll({
|
|||||||
disabled,
|
disabled,
|
||||||
loadingCallback,
|
loadingCallback,
|
||||||
completedCallback,
|
completedCallback,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateBill] = useMutation(UPDATE_BILLS);
|
const [updateBill] = useMutation(UPDATE_BILLS);
|
||||||
@@ -42,6 +43,7 @@ export function PayableExportAll({
|
|||||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(`/qbo/payables`, {
|
PartnerResponse = await axios.post(`/qbo/payables`, {
|
||||||
bills: billids,
|
bills: billids,
|
||||||
|
elgen: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let QbXmlResponse;
|
let QbXmlResponse;
|
||||||
@@ -104,57 +106,62 @@ export function PayableExportAll({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
await insertExportLog({
|
//QBO Logs are handled server side.
|
||||||
variables: {
|
await insertExportLog({
|
||||||
logs: [
|
variables: {
|
||||||
{
|
logs: [
|
||||||
bodyshopid: bodyshop.id,
|
{
|
||||||
billid: key,
|
bodyshopid: bodyshop.id,
|
||||||
successful: false,
|
billid: key,
|
||||||
message: JSON.stringify(
|
successful: false,
|
||||||
failedTransactions.map((ft) => ft.errorMessage)
|
message: JSON.stringify(
|
||||||
),
|
failedTransactions.map((ft) => ft.errorMessage)
|
||||||
useremail: currentUser.email,
|
),
|
||||||
},
|
useremail: currentUser.email,
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
billid: key,
|
|
||||||
successful: true,
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const billUpdateResponse = await updateBill({
|
|
||||||
variables: {
|
|
||||||
billIdList: [key],
|
|
||||||
bill: {
|
|
||||||
exported: true,
|
|
||||||
exported_at: new Date(),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!!!billUpdateResponse.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
key: "billsuccessexport",
|
|
||||||
message: t("bills.successes.exported"),
|
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
notification["error"]({
|
} else {
|
||||||
message: t("bills.errors.exporting", {
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
error: JSON.stringify(billUpdateResponse.error),
|
//QBO Logs are handled server side.
|
||||||
}),
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
billid: key,
|
||||||
|
successful: true,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const billUpdateResponse = await updateBill({
|
||||||
|
variables: {
|
||||||
|
billIdList: [key],
|
||||||
|
bill: {
|
||||||
|
exported: true,
|
||||||
|
exported_at: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!!!billUpdateResponse.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "billsuccessexport",
|
||||||
|
message: t("bills.successes.exported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.exporting", {
|
||||||
|
error: JSON.stringify(billUpdateResponse.error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -164,6 +171,8 @@ export function PayableExportAll({
|
|||||||
await Promise.all(proms);
|
await Promise.all(proms);
|
||||||
if (!!completedCallback) completedCallback([]);
|
if (!!completedCallback) completedCallback([]);
|
||||||
if (!!loadingCallback) loadingCallback(false);
|
if (!!loadingCallback) loadingCallback(false);
|
||||||
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export function PayableExportButton({
|
|||||||
disabled,
|
disabled,
|
||||||
loadingCallback,
|
loadingCallback,
|
||||||
setSelectedBills,
|
setSelectedBills,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateBill] = useMutation(UPDATE_BILLS);
|
const [updateBill] = useMutation(UPDATE_BILLS);
|
||||||
@@ -43,6 +44,7 @@ export function PayableExportButton({
|
|||||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(`/qbo/payables`, {
|
PartnerResponse = await axios.post(`/qbo/payables`, {
|
||||||
bills: [billId],
|
bills: [billId],
|
||||||
|
elgen: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//Default is QBD
|
//Default is QBD
|
||||||
@@ -100,64 +102,72 @@ export function PayableExportButton({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await insertExportLog({
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
variables: {
|
//QBO Logs are handled server side.
|
||||||
logs: [
|
await insertExportLog({
|
||||||
{
|
variables: {
|
||||||
bodyshopid: bodyshop.id,
|
logs: [
|
||||||
billid: billId,
|
{
|
||||||
successful: false,
|
bodyshopid: bodyshop.id,
|
||||||
message: JSON.stringify(
|
billid: billId,
|
||||||
failedTransactions.map((ft) => ft.errorMessage)
|
successful: false,
|
||||||
),
|
message: JSON.stringify(
|
||||||
useremail: currentUser.email,
|
failedTransactions.map((ft) => ft.errorMessage)
|
||||||
},
|
),
|
||||||
],
|
useremail: currentUser.email,
|
||||||
},
|
},
|
||||||
});
|
],
|
||||||
}
|
|
||||||
if (successfulTransactions.length > 0) {
|
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
billid: billId,
|
|
||||||
successful: true,
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const billUpdateResponse = await updateBill({
|
|
||||||
variables: {
|
|
||||||
billIdList: successfulTransactions.map(
|
|
||||||
(st) =>
|
|
||||||
st[
|
|
||||||
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
|
||||||
? "billid"
|
|
||||||
: "id"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
bill: {
|
|
||||||
exported: true,
|
|
||||||
exported_at: new Date(),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!!!billUpdateResponse.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
key: "billsuccessexport",
|
|
||||||
message: t("bills.successes.exported"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notification["error"]({
|
|
||||||
message: t("bills.errors.exporting", {
|
|
||||||
error: JSON.stringify(billUpdateResponse.error),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (successfulTransactions.length > 0) {
|
||||||
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
|
//QBO Logs are handled server side.
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
billid: billId,
|
||||||
|
successful: true,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const billUpdateResponse = await updateBill({
|
||||||
|
variables: {
|
||||||
|
billIdList: successfulTransactions.map(
|
||||||
|
(st) =>
|
||||||
|
st[
|
||||||
|
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
||||||
|
? "billid"
|
||||||
|
: "id"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
bill: {
|
||||||
|
exported: true,
|
||||||
|
exported_at: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!!!billUpdateResponse.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "billsuccessexport",
|
||||||
|
message: t("bills.successes.exported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.exporting", {
|
||||||
|
error: JSON.stringify(billUpdateResponse.error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||||
|
|
||||||
if (setSelectedBills) {
|
if (setSelectedBills) {
|
||||||
setSelectedBills((selectedBills) => {
|
setSelectedBills((selectedBills) => {
|
||||||
return selectedBills.filter((i) => i !== billId);
|
return selectedBills.filter((i) => i !== billId);
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { gql, useMutation } from "@apollo/client";
|
||||||
|
import { Button, notification } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
|
import {
|
||||||
|
selectBodyshop,
|
||||||
|
selectCurrentUser,
|
||||||
|
} from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(BillMarkSelectedExported);
|
||||||
|
|
||||||
|
export function BillMarkSelectedExported({
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
billids,
|
||||||
|
disabled,
|
||||||
|
loadingCallback,
|
||||||
|
completedCallback,
|
||||||
|
refetch,
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
|
const [updateBill] = useMutation(gql`
|
||||||
|
mutation UPDATE_BILL($billIds: [uuid!]!) {
|
||||||
|
update_bills(where: { id: { _in: $billIds } }, _set: { exported: true }) {
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
exported
|
||||||
|
exported_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const handleUpdate = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
loadingCallback(true);
|
||||||
|
const result = await updateBill({
|
||||||
|
variables: { billIds: billids },
|
||||||
|
update(cache) {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: billids.map((id) => {
|
||||||
|
return {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
billid: id,
|
||||||
|
successful: true,
|
||||||
|
message: JSON.stringify([t("general.labels.markedexported")]),
|
||||||
|
useremail: currentUser.email,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.errors) {
|
||||||
|
notification["success"]({
|
||||||
|
message: t("bills.successes.markexported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("bills.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loadingCallback(false);
|
||||||
|
completedCallback && completedCallback([]);
|
||||||
|
setLoading(false);
|
||||||
|
refetch && refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button loading={loading} disabled={disabled} onClick={handleUpdate}>
|
||||||
|
{t("bills.labels.markexported")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export function PaymentExportButton({
|
|||||||
disabled,
|
disabled,
|
||||||
loadingCallback,
|
loadingCallback,
|
||||||
setSelectedPayments,
|
setSelectedPayments,
|
||||||
|
refetch,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updatePayment] = useMutation(UPDATE_PAYMENTS);
|
const [updatePayment] = useMutation(UPDATE_PAYMENTS);
|
||||||
@@ -40,6 +41,7 @@ export function PaymentExportButton({
|
|||||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(`/qbo/payments`, {
|
PartnerResponse = await axios.post(`/qbo/payments`, {
|
||||||
payments: [paymentId],
|
payments: [paymentId],
|
||||||
|
elgen: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//Default is QBD
|
//Default is QBD
|
||||||
@@ -100,63 +102,68 @@ export function PaymentExportButton({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
await insertExportLog({
|
//QBO Logs are handled server side.
|
||||||
variables: {
|
await insertExportLog({
|
||||||
logs: [
|
variables: {
|
||||||
{
|
logs: [
|
||||||
bodyshopid: bodyshop.id,
|
{
|
||||||
paymentid: paymentId,
|
bodyshopid: bodyshop.id,
|
||||||
successful: false,
|
paymentid: paymentId,
|
||||||
message: JSON.stringify(
|
successful: false,
|
||||||
failedTransactions.map((ft) => ft.errorMessage)
|
message: JSON.stringify(
|
||||||
),
|
failedTransactions.map((ft) => ft.errorMessage)
|
||||||
useremail: currentUser.email,
|
),
|
||||||
},
|
useremail: currentUser.email,
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
paymentid: paymentId,
|
|
||||||
successful: true,
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const paymentUpdateResponse = await updatePayment({
|
|
||||||
variables: {
|
|
||||||
paymentIdList: successfulTransactions.map(
|
|
||||||
(st) =>
|
|
||||||
st[
|
|
||||||
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
|
||||||
? "paymentid"
|
|
||||||
: "id"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
payment: {
|
|
||||||
exportedat: new Date(),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!!!paymentUpdateResponse.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
key: "paymentsuccessexport",
|
|
||||||
message: t("payments.successes.exported"),
|
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
notification["error"]({
|
} else {
|
||||||
message: t("payments.errors.exporting", {
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
error: JSON.stringify(paymentUpdateResponse.error),
|
//QBO Logs are handled server side.
|
||||||
}),
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
paymentid: paymentId,
|
||||||
|
successful: true,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const paymentUpdateResponse = await updatePayment({
|
||||||
|
variables: {
|
||||||
|
paymentIdList: successfulTransactions.map(
|
||||||
|
(st) =>
|
||||||
|
st[
|
||||||
|
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
|
||||||
|
? "paymentid"
|
||||||
|
: "id"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
payment: {
|
||||||
|
exportedat: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!!!paymentUpdateResponse.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "paymentsuccessexport",
|
||||||
|
message: t("payments.successes.exported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("payments.errors.exporting", {
|
||||||
|
error: JSON.stringify(paymentUpdateResponse.error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setSelectedPayments) {
|
if (setSelectedPayments) {
|
||||||
@@ -165,7 +172,7 @@ export function PaymentExportButton({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||||
if (!!loadingCallback) loadingCallback(false);
|
if (!!loadingCallback) loadingCallback(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function PaymentsExportAllButton({
|
|||||||
disabled,
|
disabled,
|
||||||
loadingCallback,
|
loadingCallback,
|
||||||
completedCallback,
|
completedCallback,
|
||||||
|
refetch
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updatePayments] = useMutation(UPDATE_PAYMENTS);
|
const [updatePayments] = useMutation(UPDATE_PAYMENTS);
|
||||||
@@ -38,6 +39,7 @@ export function PaymentsExportAllButton({
|
|||||||
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(`/qbo/payments`, {
|
PartnerResponse = await axios.post(`/qbo/payments`, {
|
||||||
payments: paymentIds,
|
payments: paymentIds,
|
||||||
|
elgen: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let QbXmlResponse;
|
let QbXmlResponse;
|
||||||
@@ -92,54 +94,61 @@ export function PaymentsExportAllButton({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
await insertExportLog({
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
variables: {
|
//QBO Logs are handled server side.
|
||||||
logs: [
|
await insertExportLog({
|
||||||
{
|
variables: {
|
||||||
bodyshopid: bodyshop.id,
|
logs: [
|
||||||
paymentid: key,
|
{
|
||||||
successful: false,
|
bodyshopid: bodyshop.id,
|
||||||
message: JSON.stringify(
|
paymentid: key,
|
||||||
failedTransactions.map((ft) => ft.errorMessage)
|
successful: false,
|
||||||
),
|
message: JSON.stringify(
|
||||||
useremail: currentUser.email,
|
failedTransactions.map((ft) => ft.errorMessage)
|
||||||
},
|
),
|
||||||
],
|
useremail: currentUser.email,
|
||||||
},
|
},
|
||||||
});
|
],
|
||||||
} else {
|
|
||||||
await insertExportLog({
|
|
||||||
variables: {
|
|
||||||
logs: [
|
|
||||||
{
|
|
||||||
bodyshopid: bodyshop.id,
|
|
||||||
paymentid: key,
|
|
||||||
successful: true,
|
|
||||||
useremail: currentUser.email,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const paymentUpdateResponse = await updatePayments({
|
|
||||||
variables: {
|
|
||||||
paymentIdList: [key],
|
|
||||||
payment: {
|
|
||||||
exportedat: new Date(),
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!!!paymentUpdateResponse.errors) {
|
|
||||||
notification.open({
|
|
||||||
type: "success",
|
|
||||||
key: "paymentsuccessexport",
|
|
||||||
message: t("payments.successes.exported"),
|
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
notification["error"]({
|
} else {
|
||||||
message: t("payments.errors.exporting", {
|
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
|
||||||
error: JSON.stringify(paymentUpdateResponse.error),
|
//QBO Logs are handled server side.
|
||||||
}),
|
|
||||||
|
await insertExportLog({
|
||||||
|
variables: {
|
||||||
|
logs: [
|
||||||
|
{
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
paymentid: key,
|
||||||
|
successful: true,
|
||||||
|
useremail: currentUser.email,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
const paymentUpdateResponse = await updatePayments({
|
||||||
|
variables: {
|
||||||
|
paymentIdList: [key],
|
||||||
|
payment: {
|
||||||
|
exportedat: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!!!paymentUpdateResponse.errors) {
|
||||||
|
notification.open({
|
||||||
|
type: "success",
|
||||||
|
key: "paymentsuccessexport",
|
||||||
|
message: t("payments.successes.exported"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("payments.errors.exporting", {
|
||||||
|
error: JSON.stringify(paymentUpdateResponse.error),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@@ -148,6 +157,7 @@ export function PaymentsExportAllButton({
|
|||||||
await Promise.all(proms);
|
await Promise.all(proms);
|
||||||
if (!!completedCallback) completedCallback([]);
|
if (!!completedCallback) completedCallback([]);
|
||||||
if (!!loadingCallback) loadingCallback(false);
|
if (!!loadingCallback) loadingCallback(false);
|
||||||
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
EyeFilled,
|
EyeFilled,
|
||||||
|
DownloadOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { Card, Col, Row, Space } from "antd";
|
import { Card, Col, Row, Space } from "antd";
|
||||||
@@ -14,6 +15,7 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
|
|||||||
import "./production-board-card.styles.scss";
|
import "./production-board-card.styles.scss";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
|
|
||||||
export default function ProductionBoardCard(
|
export default function ProductionBoardCard(
|
||||||
technician,
|
technician,
|
||||||
@@ -157,6 +159,16 @@ export default function ProductionBoardCard(
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)} */}
|
)} */}
|
||||||
|
{cardSettings && cardSettings.actual_in && card.actual_in && (
|
||||||
|
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||||
|
<Space>
|
||||||
|
<DownloadOutlined />
|
||||||
|
<DateTimeFormatter format="MM/DD">
|
||||||
|
{card.actual_in}
|
||||||
|
</DateTimeFormatter>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
{cardSettings &&
|
{cardSettings &&
|
||||||
cardSettings.scheduled_completion &&
|
cardSettings.scheduled_completion &&
|
||||||
card.scheduled_completion && (
|
card.scheduled_completion && (
|
||||||
@@ -188,6 +200,11 @@ export default function ProductionBoardCard(
|
|||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
{cardSettings && cardSettings.partsstatus && (
|
||||||
|
<Col span={24}>
|
||||||
|
<JobPartsQueueCount parts={card.joblines_status} />
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ export default function ProductionBoardKanbanCardSettings({
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
valuePropName="checked"
|
||||||
|
label={t("production.labels.actual_in")}
|
||||||
|
name="actual_in"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -131,6 +138,13 @@ export default function ProductionBoardKanbanCardSettings({
|
|||||||
>
|
>
|
||||||
<Switch />
|
<Switch />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
valuePropName="checked"
|
||||||
|
label={t("production.labels.partsstatus")}
|
||||||
|
name="partsstatus"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
label={t("production.labels.stickyheader")}
|
label={t("production.labels.stickyheader")}
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ export function ProductionBoardKanbanComponent({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const boardData = createBoardData(
|
const boardData = createBoardData(
|
||||||
bodyshop.md_ro_statuses.production_statuses,
|
[
|
||||||
|
...bodyshop.md_ro_statuses.production_statuses,
|
||||||
|
...(bodyshop.md_ro_statuses.additional_board_statuses || []),
|
||||||
|
],
|
||||||
data,
|
data,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
@@ -61,13 +64,7 @@ export function ProductionBoardKanbanComponent({
|
|||||||
});
|
});
|
||||||
setBoardLanes(boardData);
|
setBoardLanes(boardData);
|
||||||
setIsMoving(false);
|
setIsMoving(false);
|
||||||
}, [
|
}, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
|
||||||
data,
|
|
||||||
setBoardLanes,
|
|
||||||
setIsMoving,
|
|
||||||
bodyshop.md_ro_statuses.production_statuses,
|
|
||||||
filter,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
|
|||||||
@@ -42,17 +42,24 @@ export function ProductionColumnsComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const columnKeys = columns.map((i) => i.key);
|
const columnKeys = columns.map((i) => i.key);
|
||||||
|
const cols = dataSource({
|
||||||
|
technician,
|
||||||
|
state: tableState,
|
||||||
|
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
||||||
|
});
|
||||||
const menu = (
|
const menu = (
|
||||||
<Menu onClick={handleAdd}>
|
<Menu
|
||||||
{dataSource({
|
onClick={handleAdd}
|
||||||
technician,
|
style={{
|
||||||
state: tableState,
|
columnCount: Math.max(Math.floor(cols.length / 10), 1),
|
||||||
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
|
}}
|
||||||
})
|
>
|
||||||
|
{cols
|
||||||
.filter((i) => !columnKeys.includes(i.key))
|
.filter((i) => !columnKeys.includes(i.key))
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<Menu.Item key={item.key}>{item.title}</Menu.Item>
|
<Menu.Item key={item.key} style={{ breakInside: "avoid" }}>
|
||||||
|
{item.title}
|
||||||
|
</Menu.Item>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, Input, Popover } from "antd";
|
import { Button, Input, Popover, Tooltip } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaRegStickyNote } from "react-icons/fa";
|
import { FaRegStickyNote } from "react-icons/fa";
|
||||||
@@ -69,10 +69,11 @@ export default function ProductionListColumnComment({ record }) {
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
|
display: "inline-block",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
|
<Icon component={FaRegStickyNote} style={{ marginRight: ".2rem" }} />
|
||||||
{record.comment || " "}
|
<Tooltip title={record.comment}>{record.comment || " "}</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,24 +5,26 @@ import moment from "moment";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { TimeFormatter } from "../../utils/DateFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||||
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||||
|
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||||
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||||
import ProductionListColumnAlert from "./production-list-columns.alert.component";
|
import ProductionListColumnAlert from "./production-list-columns.alert.component";
|
||||||
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
|
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
|
||||||
|
import ProductionListColumnComment from "./production-list-columns.comment.component";
|
||||||
import ProductionListDate from "./production-list-columns.date.component";
|
import ProductionListDate from "./production-list-columns.date.component";
|
||||||
import ProductionListColumnDetailPriority from "./production-list-columns.detailpriority.component";
|
import ProductionListColumnDetailPriority from "./production-list-columns.detailpriority.component";
|
||||||
import ProductionListEmployeeAssignment from "./production-list-columns.empassignment.component";
|
import ProductionListEmployeeAssignment from "./production-list-columns.empassignment.component";
|
||||||
import ProductionListLastContacted from "./production-list-columns.lastcontacted.component";
|
import ProductionListLastContacted from "./production-list-columns.lastcontacted.component";
|
||||||
import ProductionListColumnPaintPriority from "./production-list-columns.paintpriority.component";
|
import ProductionListColumnPaintPriority from "./production-list-columns.paintpriority.component";
|
||||||
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
|
|
||||||
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
|
||||||
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
|
||||||
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
|
||||||
import ProductionListColumnComment from "./production-list-columns.comment.component";
|
|
||||||
import ProductionListColumnPartsReceived from "./production-list-columns.partsreceived.component";
|
import ProductionListColumnPartsReceived from "./production-list-columns.partsreceived.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import ProductionListColumnNote from "./production-list-columns.productionnote.component";
|
||||||
|
import ProductionListColumnCategory from "./production-list-columns.status.category";
|
||||||
|
import ProductionListColumnStatus from "./production-list-columns.status.component";
|
||||||
|
import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.component";
|
||||||
|
|
||||||
const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||||
return [
|
return [
|
||||||
@@ -104,6 +106,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
|||||||
<ProductionListDate record={record} field="actual_in" time />
|
<ProductionListDate record={record} field="actual_in" time />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("jobs.fields.actual_in") + " (HH:MM)",
|
||||||
|
dataIndex: "actual_in_time",
|
||||||
|
key: "actual_in_time",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<TimeFormatter>{record.actual_in}</TimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18n.t("jobs.fields.scheduled_completion"),
|
title: i18n.t("jobs.fields.scheduled_completion"),
|
||||||
dataIndex: "scheduled_completion",
|
dataIndex: "scheduled_completion",
|
||||||
@@ -123,6 +135,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("jobs.fields.scheduled_completion") + " (HH:MM)",
|
||||||
|
dataIndex: "scheduled_completion_time",
|
||||||
|
key: "scheduled_completion_time",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<TimeFormatter>{record.scheduled_completion}</TimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18n.t("jobs.fields.date_last_contacted"),
|
title: i18n.t("jobs.fields.date_last_contacted"),
|
||||||
dataIndex: "date_last_contacted",
|
dataIndex: "date_last_contacted",
|
||||||
@@ -175,6 +197,16 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("jobs.fields.scheduled_delivery") + " (HH:MM)",
|
||||||
|
dataIndex: "scheduled_delivery_time",
|
||||||
|
key: "scheduled_delivery_time",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
render: (text, record) => (
|
||||||
|
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18n.t("jobs.fields.ins_co_nm"),
|
title: i18n.t("jobs.fields.ins_co_nm"),
|
||||||
dataIndex: "ins_co_nm",
|
dataIndex: "ins_co_nm",
|
||||||
@@ -490,6 +522,14 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
|||||||
<ProductionListColumnPartsReceived record={record} />
|
<ProductionListColumnPartsReceived record={record} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t("jobs.fields.partsstatus"),
|
||||||
|
dataIndex: "partsstatus",
|
||||||
|
key: "partsstatus",
|
||||||
|
render: (text, record) => (
|
||||||
|
<JobPartsQueueCount parts={record.joblines_status} record={record} />
|
||||||
|
),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
export default r;
|
export default r;
|
||||||
|
|||||||
@@ -50,50 +50,45 @@ export default function ProductionListDate({
|
|||||||
"production-completion-soon"));
|
"production-completion-soon"));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<Dropdown
|
||||||
<Dropdown
|
//trigger={["click"]}
|
||||||
//trigger={["click"]}
|
visible={visible}
|
||||||
visible={visible}
|
style={{
|
||||||
style={{
|
height: "19px",
|
||||||
height: "19px",
|
}}
|
||||||
}}
|
overlay={
|
||||||
overlay={
|
<Card style={{ padding: "1rem" }} onClick={(e) => e.stopPropagation()}>
|
||||||
<Card
|
<FormDatePicker
|
||||||
style={{ padding: "1rem" }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
value={(record[field] && moment(record[field])) || null}
|
||||||
<FormDatePicker
|
onChange={handleChange}
|
||||||
|
format="MM/DD/YYYY"
|
||||||
|
isDateOnly={!time}
|
||||||
|
/>
|
||||||
|
{time && (
|
||||||
|
<TimePicker
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
value={(record[field] && moment(record[field])) || null}
|
value={(record[field] && moment(record[field])) || null}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
format="MM/DD/YYYY"
|
minuteStep={15}
|
||||||
isDateOnly={!time}
|
format="hh:mm a"
|
||||||
/>
|
/>
|
||||||
{time && (
|
)}
|
||||||
<TimePicker
|
<Button onClick={() => setVisible(false)}>
|
||||||
onClick={(e) => e.stopPropagation()}
|
{t("general.actions.close")}
|
||||||
value={(record[field] && moment(record[field])) || null}
|
</Button>
|
||||||
onChange={handleChange}
|
</Card>
|
||||||
minuteStep={15}
|
}
|
||||||
format="hh:mm a"
|
>
|
||||||
/>
|
<div
|
||||||
)}
|
onClick={() => setVisible(true)}
|
||||||
<Button onClick={() => setVisible(false)}>
|
style={{
|
||||||
{t("general.actions.close")}
|
height: "19px",
|
||||||
</Button>
|
}}
|
||||||
</Card>
|
className={className}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div
|
<DateFormatter bordered={false}>{record[field]}</DateFormatter>
|
||||||
onClick={() => setVisible(true)}
|
</div>
|
||||||
style={{
|
</Dropdown>
|
||||||
height: "19px",
|
|
||||||
}}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
<DateFormatter bordered={false}>{record[field]}</DateFormatter>
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ import OwnerNameDisplay from "../owner-name-display/owner-name-display.component
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) =>
|
setPrintCenterContext: (context) =>
|
||||||
@@ -34,7 +36,11 @@ export default connect(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ProductionListDetail);
|
)(ProductionListDetail);
|
||||||
|
|
||||||
export function ProductionListDetail({ jobs, setPrintCenterContext }) {
|
export function ProductionListDetail({
|
||||||
|
bodyshop,
|
||||||
|
jobs,
|
||||||
|
setPrintCenterContext,
|
||||||
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { selected } = search;
|
const { selected } = search;
|
||||||
@@ -59,7 +65,7 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title={theJob.ro_number}
|
title={theJob.ro_number}
|
||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space wrap>
|
||||||
<ProductionRemoveButton jobId={theJob.id} />{" "}
|
<ProductionRemoveButton jobId={theJob.id} />{" "}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -76,6 +82,7 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) {
|
|||||||
<PrinterFilled />
|
<PrinterFilled />
|
||||||
{t("jobs.actions.printCenter")}
|
{t("jobs.actions.printCenter")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<ScoreboardAddButton job={data ? data.jobs_by_pk : {}} />
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -142,11 +149,12 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<JobDetailCardsDocumentsComponent
|
<JobDetailCardsDocumentsComponent
|
||||||
loading={loading}
|
loading={loading}
|
||||||
data={data ? data.jobs_by_pk : null}
|
data={data ? data.jobs_by_pk : null}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const ret = {
|
|||||||
"jobs:detail": 1,
|
"jobs:detail": 1,
|
||||||
"jobs:partsqueue": 4,
|
"jobs:partsqueue": 4,
|
||||||
"jobs:checklist-view": 2,
|
"jobs:checklist-view": 2,
|
||||||
|
"jobs:list-ready": 1,
|
||||||
"bills:enter": 2,
|
"bills:enter": 2,
|
||||||
"bills:view": 2,
|
"bills:view": 2,
|
||||||
"bills:list": 2,
|
"bills:list": 2,
|
||||||
@@ -66,5 +66,8 @@ const ret = {
|
|||||||
"timetickets:shiftedit": 5,
|
"timetickets:shiftedit": 5,
|
||||||
|
|
||||||
"users:editaccess": 4,
|
"users:editaccess": 4,
|
||||||
|
|
||||||
|
"inventory:list": 1,
|
||||||
|
"inventory:delete": 2,
|
||||||
};
|
};
|
||||||
export default ret;
|
export default ret;
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient, useQuery } from "@apollo/client";
|
||||||
import { GET_BLOCKED_DAYS } from "../../graphql/scoreboard.queries";
|
import {
|
||||||
|
GET_BLOCKED_DAYS,
|
||||||
|
QUERY_SCOREBOARD,
|
||||||
|
} from "../../graphql/scoreboard.queries";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -22,10 +25,15 @@ export default connect(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ScoreboardDisplayComponent);
|
)(ScoreboardDisplayComponent);
|
||||||
|
|
||||||
export function ScoreboardDisplayComponent({
|
export function ScoreboardDisplayComponent({ bodyshop }) {
|
||||||
bodyshop,
|
const scoreboardSubscription = useQuery(QUERY_SCOREBOARD, {
|
||||||
scoreboardSubscription,
|
variables: {
|
||||||
}) {
|
start: moment().startOf("month"),
|
||||||
|
end: moment().endOf("month"),
|
||||||
|
},
|
||||||
|
pollInterval: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
const { data } = scoreboardSubscription;
|
const { data } = scoreboardSubscription;
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const scoreBoardlist = (data && data.scoreboard) || [];
|
const scoreBoardlist = (data && data.scoreboard) || [];
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export default function ScoreboardEntryEdit({ entry }) {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("scoredboard.successes.updated"),
|
message: t("scoreboard.successes.updated"),
|
||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,42 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Dropdown, Button, Table, Space } from "antd";
|
import { Dropdown, Button, Table, Space, Card, Input } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
|
import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
|
import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component";
|
||||||
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
|
||||||
export default function ScoreboardJobsList({ scoreBoardlist }) {
|
export default function ScoreboardJobsList({ scoreBoardlist }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const jobs = scoreBoardlist
|
||||||
|
? searchText === ""
|
||||||
|
? scoreBoardlist
|
||||||
|
: scoreBoardlist.filter(
|
||||||
|
(sb) =>
|
||||||
|
(sb.job.ro_number || "")
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(sb.job.ownr_co_nm || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(sb.job.ownr_fn || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(sb.job.ownr_ln || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(sb.job.v_model_desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase()) ||
|
||||||
|
(sb.job.v_make_desc || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText.toLowerCase())
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -20,7 +49,25 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
|||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.owner"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
ellipsis: true,
|
||||||
|
|
||||||
|
render: (text, record) => <OwnerNameDisplay ownerObject={record.job} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.vehicle"),
|
||||||
|
dataIndex: "vehicle",
|
||||||
|
key: "vehicle",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text, record) => (
|
||||||
|
<span>{`${record.job.v_model_yr || ""} ${
|
||||||
|
record.job.v_make_desc || ""
|
||||||
|
} ${record.job.v_model_desc || ""}`}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("scoreboard.fields.date"),
|
title: t("scoreboard.fields.date"),
|
||||||
dataIndex: "date",
|
dataIndex: "date",
|
||||||
@@ -51,17 +98,29 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const overlay = (
|
const overlay = (
|
||||||
<div style={{ width: "50vw", padding: "1rem" }}>
|
<Card
|
||||||
|
style={{ maxWidth: "90vw", padding: "1rem" }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
extra={
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
}}
|
||||||
|
value={searchText}
|
||||||
|
enterButton
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Table
|
<Table
|
||||||
pagination={false}
|
pagination={false}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={scoreBoardlist}
|
dataSource={jobs}
|
||||||
scroll={{ x: true, y: "15rem" }}
|
|
||||||
style={{ padding: "1rem" }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
|
|
||||||
const values = useMemo(() => {
|
const values = useMemo(() => {
|
||||||
const dateHash = _.groupBy(scoreBoardlist, "date");
|
const dateHash = _.groupBy(scoreBoardlist, "date");
|
||||||
console.log(
|
|
||||||
"🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 31 ~ values ~ dateHash",
|
|
||||||
dateHash
|
|
||||||
);
|
|
||||||
|
|
||||||
let ret = {
|
let ret = {
|
||||||
todayBody: 0,
|
todayBody: 0,
|
||||||
@@ -71,10 +67,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}, [scoreBoardlist]);
|
}, [scoreBoardlist]);
|
||||||
console.log(
|
|
||||||
"🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 51 ~ values ~ values",
|
|
||||||
values
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -185,6 +177,27 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) {
|
|||||||
<Statistic value={values.toDatePaint.toFixed(1)} />
|
<Statistic value={values.toDatePaint.toFixed(1)} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col {...statSpans}></Col>
|
||||||
|
<Col {...statSpans}>
|
||||||
|
<Statistic
|
||||||
|
value={(values.todayPaint + values.todayBody).toFixed(1)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col {...statSpans}></Col>
|
||||||
|
<Col {...statSpans}>
|
||||||
|
<Statistic
|
||||||
|
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col {...statSpans}></Col>
|
||||||
|
<Col {...statSpans}></Col>
|
||||||
|
<Col {...statSpans}>
|
||||||
|
<Statistic
|
||||||
|
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -47,3 +47,15 @@ export const ListOfDaysInCurrentMonth = () => {
|
|||||||
days.push(dateEnd.format("YYYY-MM-DD"));
|
days.push(dateEnd.format("YYYY-MM-DD"));
|
||||||
return days;
|
return days;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ListDaysBetween = ({ start, end }) => {
|
||||||
|
const days = [];
|
||||||
|
const dateStart = moment(start);
|
||||||
|
const dateEnd = moment(end);
|
||||||
|
while (dateEnd.diff(dateStart, "days") > 0) {
|
||||||
|
days.push(dateStart.format("YYYY-MM-DD"));
|
||||||
|
dateStart.add(1, "days");
|
||||||
|
}
|
||||||
|
days.push(dateEnd.format("YYYY-MM-DD"));
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user