Compare commits
80 Commits
release/20
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1881671a8 | ||
|
|
e0e88acb9e | ||
|
|
14c40958b4 | ||
|
|
2438857cb7 | ||
|
|
9362372512 | ||
|
|
b30192c6d5 | ||
|
|
9ddcb1f27d | ||
|
|
cd29a0f613 | ||
|
|
b189b045f8 | ||
|
|
17baca40d9 | ||
|
|
956134c360 | ||
|
|
d5f55c5873 | ||
|
|
179147dc4e | ||
|
|
385de3eaaa | ||
|
|
22368069a4 | ||
|
|
5eb8c2c03a | ||
|
|
a23b3bf02d | ||
|
|
4976e6be95 | ||
|
|
1b58a29112 | ||
|
|
05e236cd9c | ||
|
|
90bd70070d | ||
|
|
fb6c667a7f | ||
|
|
c048f21674 | ||
|
|
05de47c833 | ||
|
|
c163e2a274 | ||
|
|
7cf32775eb | ||
|
|
27ce30527e | ||
|
|
b346c28fe0 | ||
|
|
05a0ee30f4 | ||
|
|
7a0d5d712a | ||
|
|
4802c1abe8 | ||
|
|
7022609e22 | ||
|
|
d8447b1197 | ||
|
|
43b140aed4 | ||
|
|
d4ee6ca8ba | ||
|
|
d6673ed278 | ||
|
|
e0804099ee | ||
|
|
ece0946738 | ||
|
|
9bcc44c0cc | ||
|
|
b9b6759c54 | ||
|
|
1f1274a54a | ||
|
|
35d4188469 | ||
|
|
a8f89c81fc | ||
|
|
c382b3f2e0 | ||
|
|
39dbf40a49 | ||
|
|
037ff4c2a1 | ||
|
|
40037216aa | ||
|
|
5a6a92c260 | ||
|
|
fb4b12233a | ||
|
|
d45f84afbd | ||
|
|
1d80153da1 | ||
|
|
f704fd5f56 | ||
|
|
82c4320f0c | ||
|
|
bebe99f4e6 | ||
|
|
1e24d5d57f | ||
|
|
fbcf2b559e | ||
|
|
4582c493ee | ||
|
|
ecfd284539 | ||
|
|
2a1c046dd6 | ||
|
|
54ebc2e25b | ||
|
|
974a0ec1f1 | ||
|
|
acf99584ea | ||
|
|
d0d4ceb270 | ||
|
|
f2e7808fa0 | ||
|
|
c07458babf | ||
|
|
0892461631 | ||
|
|
623d407a6c | ||
|
|
5e3218a145 | ||
|
|
706f300750 | ||
|
|
4fad4e41c2 | ||
|
|
1e88d5ae1b | ||
|
|
7ba3cc5ffa | ||
|
|
4fdd48c279 | ||
|
|
f5834ae6bc | ||
|
|
db36b27819 | ||
|
|
43fbf32e99 | ||
|
|
1b2afb9e93 | ||
|
|
6a109d63ce | ||
|
|
a6610309e9 | ||
|
|
db02b9c1c2 |
@@ -2063,6 +2063,32 @@
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
<name>validation</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>atleastone</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
@@ -8138,6 +8164,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>default_quote</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>default_received</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -11102,6 +11149,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>dateinpast</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>dlexpirebeforereturn</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -12323,6 +12391,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>leasereturn</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>out</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -14688,6 +14777,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>vacationadded</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
@@ -15374,6 +15484,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>sizelimit</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
@@ -17758,6 +17889,27 @@
|
||||
<folder_node>
|
||||
<name>actions</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>converttolabor</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>new</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -18938,6 +19090,27 @@
|
||||
<folder_node>
|
||||
<name>labels</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>adjustmenttobeadded</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>billref</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -18959,6 +19132,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>convertedtolabor</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>edit</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -26420,6 +26614,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>convertedtolabor</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>cost</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -27191,6 +27406,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>estimator</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>existing_jobs</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -32391,6 +32627,48 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>systemnotes</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>usernotes</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
@@ -32547,6 +32825,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>saving</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>selectexistingornew</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -32951,6 +33250,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>tax_number</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
@@ -33946,6 +34266,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>is_quote</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>mark_as_received</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -40025,6 +40366,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>jobs_reconcile</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>lag_time</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -41152,6 +41514,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>efficiencyoverperiod</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>jobs</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -44929,6 +45312,27 @@
|
||||
<folder_node>
|
||||
<name>signinerror</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>auth/user-disabled</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>auth/user-not-found</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@sentry/react": "^7.1.1",
|
||||
"@sentry/tracing": "^7.1.1",
|
||||
"@splitsoftware/splitio-react": "^1.4.1",
|
||||
|
||||
@@ -142,3 +142,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Update row highlighting on production board.
|
||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
||||
background: #eaeaea !important;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-it
|
||||
import "./bill-cm-returns-table.styles.scss";
|
||||
export default function BillCmdReturnsTableComponent({
|
||||
form,
|
||||
loadOutstandingReturns,
|
||||
returnLoading,
|
||||
returnData,
|
||||
}) {
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
|
||||
import moment from "moment";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
DELETE_BILL_LINE,
|
||||
INSERT_NEW_BILL_LINES,
|
||||
UPDATE_BILL_LINE
|
||||
} from "../../graphql/bill-lines.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 BillFormContainer from "../bill-form/bill-form.container";
|
||||
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";
|
||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillDetailEditcontainer);
|
||||
|
||||
export function BillDetailEditcontainer({
|
||||
setPartsOrderContext,
|
||||
insertAuditTrail,
|
||||
bodyshop,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [update_bill] = useMutation(UPDATE_BILL);
|
||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
|
||||
variables: { billid: search.billid },
|
||||
skip: !!!search.billid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
//It's got a previously deducted bill line!
|
||||
if (
|
||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
||||
0
|
||||
)
|
||||
setVisible(true);
|
||||
else {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setUpdateLoading(true);
|
||||
//let adjustmentsToInsert = {};
|
||||
|
||||
const { billlines, upload, ...bill } = values;
|
||||
const updates = [];
|
||||
updates.push(
|
||||
update_bill({
|
||||
variables: { billId: search.billid, bill: bill },
|
||||
})
|
||||
);
|
||||
|
||||
billlines.forEach((l) => {
|
||||
delete l.selected;
|
||||
});
|
||||
|
||||
//Find bill lines that were deleted.
|
||||
const deletedJobLines = [];
|
||||
|
||||
data.bills_by_pk.billlines.forEach((a) => {
|
||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||
if (!matchingRecord) {
|
||||
deletedJobLines.push(a);
|
||||
}
|
||||
});
|
||||
|
||||
deletedJobLines.forEach((d) => {
|
||||
updates.push(deleteBillLine({ variables: { id: d.id } }));
|
||||
});
|
||||
|
||||
billlines.forEach((billline) => {
|
||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
updates.push(
|
||||
updateBillLine({
|
||||
variables: {
|
||||
billLineId: il.id,
|
||||
billLine: {
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
//It's a new line, have to insert it.
|
||||
updates.push(
|
||||
insertBillLine({
|
||||
variables: {
|
||||
billLines: [
|
||||
{
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
billid: search.billid,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
});
|
||||
|
||||
await refetch();
|
||||
form.setFieldsValue(transformData(data));
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
setUpdateLoading(false);
|
||||
};
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <LoadingSkeleton />}
|
||||
{data && (
|
||||
<>
|
||||
<PageHeader
|
||||
title={
|
||||
data &&
|
||||
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<BillDetailEditReturn data={data} />
|
||||
|
||||
<Popconfirm
|
||||
visible={visible}
|
||||
onConfirm={() => form.submit()}
|
||||
onCancel={() => setVisible(false)}
|
||||
okButtonProps={{ loading: updateLoading }}
|
||||
title={t("bills.labels.editadjwarning")}
|
||||
>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={exported}
|
||||
onClick={handleSave}
|
||||
loading={updateLoading}
|
||||
type="primary"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
|
||||
<BillMarkExportedButton bill={data && data.bills_by_pk} />
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={transformData(data)}
|
||||
layout="vertical"
|
||||
>
|
||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const transformData = (data) => {
|
||||
return data
|
||||
? {
|
||||
...data.bills_by_pk,
|
||||
|
||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
|
||||
}
|
||||
: {};
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Button, Checkbox, Form, Modal } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillDetailEditReturn);
|
||||
|
||||
export function BillDetailEditReturn({
|
||||
setPartsOrderContext,
|
||||
insertAuditTrail,
|
||||
bodyshop,
|
||||
data,
|
||||
disabled,
|
||||
}) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const handleFinish = ({ billlines }) => {
|
||||
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
||||
|
||||
setPartsOrderContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: data.bills_by_pk.jobid,
|
||||
vendorId: data.bills_by_pk.vendorid,
|
||||
returnFromBill: data.bills_by_pk.id,
|
||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||
linesToOrder: data.bills_by_pk.billlines
|
||||
.filter((l) => selectedLines.includes(l.id))
|
||||
.map((i) => {
|
||||
return {
|
||||
line_desc: i.line_desc,
|
||||
// db_price: i.actual_price,
|
||||
act_price: i.actual_price,
|
||||
cost: i.actual_cost,
|
||||
quantity: i.quantity,
|
||||
joblineid: i.joblineid,
|
||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||
part_type: i.jobline && i.jobline.part_type,
|
||||
};
|
||||
}),
|
||||
isReturn: true,
|
||||
},
|
||||
});
|
||||
delete search.billid;
|
||||
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
setVisible(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (visible === false) form.resetFields();
|
||||
}, [visible, form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(false)}
|
||||
destroyOnClose
|
||||
title={t("bills.actions.return")}
|
||||
onOk={() => form.submit()}
|
||||
>
|
||||
<Form
|
||||
initialValues={data && data.bills_by_pk}
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
>
|
||||
<Form.List name={["billlines"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<table style={{ tableLayout: "auto", width: "100%" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<Checkbox
|
||||
onChange={(e) => {
|
||||
form.setFieldsValue({
|
||||
billlines: form
|
||||
.getFieldsValue()
|
||||
.billlines.map((b) => ({
|
||||
...b,
|
||||
selected: e.target.checked,
|
||||
})),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>{t("billlines.fields.line_desc")}</td>
|
||||
<td>{t("billlines.fields.quantity")}</td>
|
||||
<td>{t("billlines.fields.actual_price")}</td>
|
||||
<td>{t("billlines.fields.actual_cost")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fields.map((field, index) => (
|
||||
<tr key={field.key}>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.selected")}
|
||||
key={`${index}selected`}
|
||||
name={[field.name, "selected"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<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
|
||||
// label={t("joblines.fields.quantity")}
|
||||
key={`${index}quantity`}
|
||||
name={[field.name, "quantity"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.actual_price")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
// label={t("joblines.fields.actual_cost")}
|
||||
key={`${index}actual_cost`}
|
||||
name={[field.name, "actual_cost"]}
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Button
|
||||
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{t("bills.actions.return")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +1,12 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
Form,
|
||||
Grid,
|
||||
PageHeader,
|
||||
Popconfirm,
|
||||
Space,
|
||||
} from "antd";
|
||||
import moment from "moment";
|
||||
import { Drawer, Grid } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import React from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
DELETE_BILL_LINE,
|
||||
INSERT_NEW_BILL_LINES,
|
||||
UPDATE_BILL_LINE,
|
||||
} from "../../graphql/bill-lines.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 BillFormContainer from "../bill-form/bill-form.container";
|
||||
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";
|
||||
import BillDetailEditComponent from "./bill-detail-edit-component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(BillDetailEditcontainer);
|
||||
|
||||
export function BillDetailEditcontainer({
|
||||
setPartsOrderContext,
|
||||
insertAuditTrail,
|
||||
bodyshop,
|
||||
}) {
|
||||
export default function BillDetailEditcontainer() {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [update_bill] = useMutation(UPDATE_BILL);
|
||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
||||
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
@@ -80,114 +24,6 @@ export function BillDetailEditcontainer({
|
||||
? bpoints[selectedBreakpoint[0]]
|
||||
: "100%";
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
|
||||
variables: { billid: search.billid },
|
||||
skip: !!!search.billid,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
//It's got a previously deducted bill line!
|
||||
if (
|
||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
||||
0
|
||||
)
|
||||
setVisible(true);
|
||||
else {
|
||||
form.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setUpdateLoading(true);
|
||||
//let adjustmentsToInsert = {};
|
||||
|
||||
const { billlines, upload, ...bill } = values;
|
||||
const updates = [];
|
||||
updates.push(
|
||||
update_bill({
|
||||
variables: { billId: search.billid, bill: bill },
|
||||
})
|
||||
);
|
||||
|
||||
//Find bill lines that were deleted.
|
||||
const deletedJobLines = [];
|
||||
|
||||
data.bills_by_pk.billlines.forEach((a) => {
|
||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||
if (!matchingRecord) {
|
||||
deletedJobLines.push(a);
|
||||
}
|
||||
});
|
||||
|
||||
deletedJobLines.forEach((d) => {
|
||||
updates.push(deleteBillLine({ variables: { id: d.id } }));
|
||||
});
|
||||
|
||||
billlines.forEach((billline) => {
|
||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
||||
delete il.__typename;
|
||||
|
||||
if (il.id) {
|
||||
updates.push(
|
||||
updateBillLine({
|
||||
variables: {
|
||||
billLineId: il.id,
|
||||
billLine: {
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
} else {
|
||||
//It's a new line, have to insert it.
|
||||
updates.push(
|
||||
insertBillLine({
|
||||
variables: {
|
||||
billLines: [
|
||||
{
|
||||
...il,
|
||||
deductedfromlbr: deductedfromlbr,
|
||||
billid: search.billid,
|
||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: bill.jobid,
|
||||
billid: search.billid,
|
||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||
});
|
||||
|
||||
await refetch();
|
||||
form.setFieldsValue(transformData(data));
|
||||
form.resetFields();
|
||||
setVisible(false);
|
||||
setUpdateLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (search.billid && data) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [form, search.billid, data]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width={drawerPercentage}
|
||||
@@ -195,119 +31,10 @@ export function BillDetailEditcontainer({
|
||||
delete search.billid;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
destroyOnClose
|
||||
visible={search.billid}
|
||||
>
|
||||
{loading && <LoadingSkeleton />}
|
||||
{!loading && (
|
||||
<>
|
||||
<PageHeader
|
||||
title={
|
||||
data &&
|
||||
`${data.bills_by_pk.invoice_number} - ${data.bills_by_pk.vendor.name}`
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
disabled={data.bills_by_pk.is_credit_memo}
|
||||
onClick={() => {
|
||||
delete search.billid;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
setPartsOrderContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: data.bills_by_pk.jobid,
|
||||
vendorId: data.bills_by_pk.vendorid,
|
||||
returnFromBill: data.bills_by_pk.id,
|
||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||
linesToOrder: data.bills_by_pk.billlines.map((i) => {
|
||||
return {
|
||||
line_desc: i.line_desc,
|
||||
// db_price: i.actual_price,
|
||||
act_price: i.actual_price,
|
||||
cost: i.actual_cost,
|
||||
quantity: i.quantity,
|
||||
joblineid: i.joblineid,
|
||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||
part_type: i.jobline && i.jobline.part_type,
|
||||
};
|
||||
}),
|
||||
isReturn: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("bills.actions.return")}
|
||||
</Button>
|
||||
|
||||
<Popconfirm
|
||||
visible={visible}
|
||||
onConfirm={() => form.submit()}
|
||||
onCancel={() => setVisible(false)}
|
||||
okButtonProps={{ loading: updateLoading }}
|
||||
title={t("bills.labels.editadjwarning")}
|
||||
>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
disabled={exported}
|
||||
onClick={handleSave}
|
||||
loading={updateLoading}
|
||||
type="primary"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
|
||||
<BillMarkExportedButton bill={data && data.bills_by_pk} />
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={transformData(data)}
|
||||
layout="vertical"
|
||||
>
|
||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
<BillDetailEditComponent />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
const transformData = (data) => {
|
||||
return data
|
||||
? {
|
||||
...data.bills_by_pk,
|
||||
|
||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||
return {
|
||||
...i,
|
||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||
applicable_taxes: {
|
||||
federal:
|
||||
(i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
|
||||
},
|
||||
};
|
||||
}),
|
||||
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
|
||||
}
|
||||
: {};
|
||||
};
|
||||
|
||||
@@ -144,6 +144,14 @@ function BillEnterModalContainer({
|
||||
adjKeys.forEach((key) => {
|
||||
newAdjustments[key] =
|
||||
(newAdjustments[key] || 0) + adjustmentsToInsert[key];
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
mod_lbr_ty: key,
|
||||
hours: adjustmentsToInsert[key].toFixed(1),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
@@ -161,10 +169,6 @@ function BillEnterModalContainer({
|
||||
});
|
||||
return;
|
||||
}
|
||||
insertAuditTrail({
|
||||
jobid: values.jobid,
|
||||
operation: AuditTrailMapping.jobmodifylbradj(),
|
||||
});
|
||||
}
|
||||
|
||||
const markPolReceived =
|
||||
|
||||
@@ -58,6 +58,7 @@ export function BillFormComponent({
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
|
||||
const handleVendorSelect = (props, opt) => {
|
||||
setDiscount(opt.discount);
|
||||
|
||||
@@ -140,13 +141,14 @@ export function BillFormComponent({
|
||||
onBlur={() => {
|
||||
if (form.getFieldValue("jobid") !== null) {
|
||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
||||
if (form.getFieldValue("vendorid") !== null)
|
||||
if (form.getFieldValue("vendorid") !== null) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
vendorId: form.getFieldValue("vendorid"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -273,12 +275,13 @@ export function BillFormComponent({
|
||||
getFieldValue("jobid") &&
|
||||
getFieldValue("vendorid")
|
||||
) {
|
||||
loadOutstandingReturns({
|
||||
variables: {
|
||||
jobId: form.getFieldValue("jobid"),
|
||||
vendorId: form.getFieldValue("vendorid"),
|
||||
},
|
||||
});
|
||||
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
||||
// loadOutstandingReturns({
|
||||
// variables: {
|
||||
// jobId: form.getFieldValue("jobid"),
|
||||
// vendorId: form.getFieldValue("vendorid"),
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -311,15 +314,17 @@ export function BillFormComponent({
|
||||
>
|
||||
<CurrencyInput min={0} disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{!billEdit && (
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item
|
||||
|
||||
@@ -63,7 +63,6 @@ export function BillFormContainer({
|
||||
{!billEdit && (
|
||||
<BillCmdReturnsTableComponent
|
||||
form={form}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
returnLoading={returnLoading}
|
||||
returnData={returnData}
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Button, Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tooltip,
|
||||
Tooltip
|
||||
} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -17,9 +17,8 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CiecaSelect from "../../utils/Ciecaselect";
|
||||
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.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";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -323,28 +322,31 @@ export function BillEnterModalLinesComponent({
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: t("billlines.fields.location"),
|
||||
dataIndex: "location",
|
||||
editable: true,
|
||||
label: t("billlines.fields.location"),
|
||||
formItemProps: (field) => {
|
||||
return {
|
||||
key: `${field.index}location`,
|
||||
name: [field.name, "location"],
|
||||
};
|
||||
},
|
||||
formInput: (record, index) => (
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
...(billEdit
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("billlines.fields.location"),
|
||||
dataIndex: "location",
|
||||
editable: true,
|
||||
label: t("billlines.fields.location"),
|
||||
formItemProps: (field) => {
|
||||
return {
|
||||
key: `${field.index}location`,
|
||||
name: [field.name, "location"],
|
||||
};
|
||||
},
|
||||
formInput: (record, index) => (
|
||||
<Select disabled={disabled}>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
]),
|
||||
{
|
||||
title: t("billlines.labels.deductedfromlbr"),
|
||||
dataIndex: "deductedfromlbr",
|
||||
@@ -552,7 +554,20 @@ export function BillEnterModalLinesComponent({
|
||||
});
|
||||
|
||||
return (
|
||||
<Form.List name="billlines">
|
||||
<Form.List
|
||||
name="billlines"
|
||||
rules={[
|
||||
{
|
||||
validator: async (_, billlines) => {
|
||||
if (!billlines || billlines.length < 1) {
|
||||
return Promise.reject(
|
||||
new Error(t("billlines.validation.atleastone"))
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(fields, { add, remove, move }) => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -58,39 +59,15 @@ export function BillsListTableComponent({
|
||||
</Button>
|
||||
)}
|
||||
<BillDeleteButton bill={record} />
|
||||
<Button
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
disabled={
|
||||
record.is_credit_memo ||
|
||||
record.vendorid === bodyshop.inhousevendorid ||
|
||||
jobRO
|
||||
}
|
||||
onClick={() => {
|
||||
setPartsOrderContext({
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: job.id,
|
||||
vendorId: record.vendorid,
|
||||
returnFromBill: record.id,
|
||||
invoiceNumber: record.invoice_number,
|
||||
linesToOrder: record.billlines.map((i) => {
|
||||
return {
|
||||
line_desc: i.line_desc,
|
||||
// db_price: i.actual_price,
|
||||
act_price: i.actual_price,
|
||||
cost: i.actual_cost,
|
||||
quantity: i.quantity,
|
||||
joblineid: i.joblineid,
|
||||
oem_partno: i.jobline && i.jobline.oem_partno,
|
||||
part_type: i.jobline && i.jobline.part_type,
|
||||
};
|
||||
}),
|
||||
isReturn: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("bills.actions.return")}
|
||||
</Button>
|
||||
/>
|
||||
|
||||
{record.isinhouse && (
|
||||
<PrintWrapperComponent
|
||||
templateObject={{
|
||||
|
||||
@@ -279,31 +279,80 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) {
|
||||
<Form.Item label={t("courtesycars.fields.notes")} name="notes">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.registrationexpires")}
|
||||
name="registrationexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) =>
|
||||
p.registrationexpires !== c.registrationexpires
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const expires = form.getFieldValue("registrationexpires");
|
||||
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.registrationexpires")}
|
||||
name="registrationexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
const dateover = expires && moment(expires).isBefore(moment());
|
||||
|
||||
if (dateover)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.dateinpast")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div>
|
||||
<Form.Item
|
||||
label={t("courtesycars.fields.insuranceexpires")}
|
||||
name="insuranceexpires"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
shouldUpdate={(p, c) =>
|
||||
p.insuranceexpires !== c.insuranceexpires
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const expires = form.getFieldValue("insuranceexpires");
|
||||
|
||||
const dateover = expires && moment(expires).isBefore(moment());
|
||||
|
||||
if (dateover)
|
||||
return (
|
||||
<Space direction="vertical" style={{ color: "tomato" }}>
|
||||
<span>
|
||||
<WarningFilled style={{ marginRight: ".3rem" }} />
|
||||
{t("contracts.labels.dateinpast")}
|
||||
</span>
|
||||
</Space>
|
||||
);
|
||||
|
||||
return <></>;
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item label={t("courtesycars.fields.dailycost")} name="dailycost">
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
|
||||
@@ -34,6 +34,9 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
<Option value="courtesycars.status.sold">
|
||||
{t("courtesycars.status.sold")}
|
||||
</Option>
|
||||
<Option value="courtesycars.status.leasereturn">
|
||||
{t("courtesycars.status.leasereturn")}
|
||||
</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
text: t("courtesycars.status.sold"),
|
||||
value: "courtesycars.status.sold",
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
sortOrder:
|
||||
|
||||
@@ -54,7 +54,7 @@ export const uploadToCloudinary = async (
|
||||
//Set variables for getting the signed URL.
|
||||
let timestamp = Math.floor(Date.now() / 1000);
|
||||
let public_id = key;
|
||||
let tags = `${bodyshop.textid},${
|
||||
let tags = `${bodyshop.imexshopid},${
|
||||
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
|
||||
}`;
|
||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||
|
||||
@@ -23,7 +23,7 @@ export default connect(
|
||||
|
||||
export function EmailDocumentsComponent({
|
||||
emailConfig,
|
||||
|
||||
form,
|
||||
selectedMediaState,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -45,6 +45,13 @@ export function EmailDocumentsComponent({
|
||||
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
|
||||
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
|
||||
) : null}
|
||||
{selectedMedia &&
|
||||
selectedMedia
|
||||
.filter((s) => s.isSelected)
|
||||
.reduce((acc, val) => (acc = acc + val.size), 0) >=
|
||||
10485760 - new Blob([form.getFieldValue("html")]).size ? (
|
||||
<div style={{ color: "red" }}>{t("general.errors.sizelimit")}</div>
|
||||
) : null}
|
||||
{data && (
|
||||
<JobDocumentsGalleryExternal
|
||||
data={data ? data.documents : []}
|
||||
|
||||
@@ -162,7 +162,10 @@ export function EmailOverlayComponent({
|
||||
<Tabs>
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
|
||||
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
|
||||
<EmailDocumentsComponent
|
||||
selectedMediaState={selectedMediaState}
|
||||
form={form}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
|
||||
@@ -180,6 +183,24 @@ export function EmailOverlayComponent({
|
||||
}
|
||||
return e && e.fileList;
|
||||
}}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
const totalSize = value.reduce(
|
||||
(acc, val) => (acc = acc + val.size),
|
||||
0
|
||||
);
|
||||
|
||||
const limit =
|
||||
10485760 - new Blob([form.getFieldValue("html")]).size;
|
||||
|
||||
if (totalSize > limit) {
|
||||
return Promise.reject(t("general.errors.sizelimit"));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Upload.Dragger
|
||||
beforeUpload={Upload.LIST_IGNORE}
|
||||
|
||||
@@ -168,7 +168,6 @@ export function EmailOverlayContainer({
|
||||
useEffect(() => {
|
||||
if (modalVisible) render();
|
||||
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose={true}
|
||||
@@ -178,7 +177,15 @@ export function EmailOverlayContainer({
|
||||
onCancel={() => {
|
||||
toggleEmailOverlayVisible();
|
||||
}}
|
||||
okButtonProps={{ loading: sending }}
|
||||
okButtonProps={{
|
||||
loading: sending,
|
||||
disabled:
|
||||
selectedMedia &&
|
||||
( (selectedMedia
|
||||
.filter((s) => s.isSelected)
|
||||
.reduce((acc, val) => (acc = acc + val.size), 0) >=
|
||||
10485760 - new Blob([form.getFieldValue("html")]).size) || selectedMedia.filter((s) => s.isSelected).length > 10),
|
||||
}}
|
||||
>
|
||||
<Form layout="vertical" form={form} onFinish={handleFinish}>
|
||||
{loading && (
|
||||
|
||||
@@ -30,6 +30,7 @@ class ErrorBoundary extends React.Component {
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
console.log("ErrorBoundary -> getDerivedStateFromError -> error", error);
|
||||
|
||||
return { hasErrored: true, error: error };
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import { Prompt, useLocation } from "react-router-dom";
|
||||
import "./form-fields-changed.styles.scss";
|
||||
|
||||
export default function FormsFieldChanged({ form }) {
|
||||
export default function FormsFieldChanged({ form, skipPrompt }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleReset = () => {
|
||||
@@ -25,7 +25,7 @@ export default function FormsFieldChanged({ form }) {
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Prompt
|
||||
when={true}
|
||||
when={skipPrompt ? false : true}
|
||||
message={(location) => {
|
||||
if (loc.pathname === location.pathname) return false;
|
||||
return t("general.messages.unsavedchangespopup");
|
||||
|
||||
@@ -11,6 +11,7 @@ import AlertComponent from "../alert/alert.component";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
@@ -97,7 +98,11 @@ export default function GlobalSearch() {
|
||||
} ${vehicle.v_model_desc || ""}`}
|
||||
</span>
|
||||
<span>{vehicle.plate_no || ""}</span>
|
||||
<span> {vehicle.v_vin || ""}</span>
|
||||
<span>
|
||||
<VehicleVinDisplay>
|
||||
{vehicle.v_vin || ""}
|
||||
</VehicleVinDisplay>
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
|
||||
@@ -247,40 +247,40 @@ export function ScheduleEventComponent({
|
||||
);
|
||||
|
||||
const RegularEvent = event.isintake ? (
|
||||
<div
|
||||
<Space
|
||||
wrap
|
||||
size='small'
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
height: "100%",
|
||||
backgroundColor:
|
||||
event.color && event.color.hex ? event.color.hex : event.color,
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
{event.note && <AlertFilled className="production-alert" />}
|
||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={event.job} />
|
||||
</span>
|
||||
</Space>
|
||||
<Space>
|
||||
<span>
|
||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||
(event.job && event.job.v_make_desc) || ""
|
||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||
</span>
|
||||
<span>
|
||||
{`(${
|
||||
(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||
} / ${
|
||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||
})`}
|
||||
</span>
|
||||
</Space>
|
||||
{event.note && <AlertFilled className="production-alert" />}
|
||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||
|
||||
<OwnerNameDisplay ownerObject={event.job} />
|
||||
|
||||
{`${(event.job && event.job.v_model_yr) || ""} ${
|
||||
(event.job && event.job.v_make_desc) || ""
|
||||
} ${(event.job && event.job.v_model_desc) || ""}`}
|
||||
|
||||
{`(${(event.job && event.job.labhrs.aggregate.sum.mod_lb_hrs) || "0"} / ${
|
||||
(event.job && event.job.larhrs.aggregate.sum.mod_lb_hrs) || "0"
|
||||
})`}
|
||||
|
||||
{event.job && event.job.alt_transport && (
|
||||
<div style={{ margin: ".1rem" }}>{event.job.alt_transport}</div>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
) : (
|
||||
<div style={{ height: "100%", width: "100%" }}>
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor:
|
||||
event.color && event.color.hex ? event.color.hex : event.color,
|
||||
}}
|
||||
>
|
||||
<strong>{`${event.title || ""}`}</strong>
|
||||
</div>
|
||||
);
|
||||
@@ -291,7 +291,13 @@ export function ScheduleEventComponent({
|
||||
onVisibleChange={(vis) => !event.vacation && setVisible(vis)}
|
||||
trigger="click"
|
||||
content={event.block ? blockContent : popoverContent}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
|
||||
backgroundColor:
|
||||
event.color && event.color.hex ? event.color.hex : event.color,
|
||||
}}
|
||||
>
|
||||
{RegularEvent}
|
||||
</Popover>
|
||||
|
||||
@@ -44,6 +44,7 @@ import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import moment from "moment";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -114,7 +115,10 @@ export function JobLinesComponent({
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) => record.oem_partno,
|
||||
render: (text, record) =>
|
||||
`${record.oem_partno || ""} ${
|
||||
record.alt_partno ? `(${record.alt_partno})` : ""
|
||||
}`.trim(),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.op_code_desc"),
|
||||
@@ -175,7 +179,7 @@ export function JobLinesComponent({
|
||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<JobLineConvertToLabor jobline={record} job={job}>
|
||||
<CurrencyFormatter>
|
||||
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||
? record.prt_dsmk_m
|
||||
@@ -188,7 +192,7 @@ export function JobLinesComponent({
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</JobLineConvertToLabor>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -295,9 +299,9 @@ export function JobLinesComponent({
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<div>
|
||||
<Space>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
<Space>
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={() => {
|
||||
@@ -334,9 +338,9 @@ export function JobLinesComponent({
|
||||
>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -460,7 +464,12 @@ export function JobLinesComponent({
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
linesToOrder: selectedLines,
|
||||
linesToOrder: selectedLines.map((l) => ({
|
||||
...l,
|
||||
oem_partno: `${l.oem_partno || ""} ${
|
||||
l.alt_partno ? `(${l.alt_partno})` : ""
|
||||
}`.trim(),
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -476,7 +485,18 @@ export function JobLinesComponent({
|
||||
setState({
|
||||
...state,
|
||||
filteredInfo: {
|
||||
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"],
|
||||
part_type: [
|
||||
"PAN",
|
||||
"PAC",
|
||||
"PAR",
|
||||
"PAL",
|
||||
"PAA",
|
||||
"PAM",
|
||||
"PAP",
|
||||
"PAS",
|
||||
"PASL",
|
||||
"PAG",
|
||||
],
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { ClockCircleOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
notification,
|
||||
Popover,
|
||||
Select,
|
||||
Space,
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
UPDATE_JOB,
|
||||
} from "../../graphql/jobs.queries";
|
||||
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobLineConvertToLabor);
|
||||
|
||||
export function JobLineConvertToLabor({
|
||||
children,
|
||||
jobline,
|
||||
job,
|
||||
insertAuditTrail,
|
||||
...otherBtnProps
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const [visibility, setVisibility] = useState(false);
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
const { mod_lbr_ty } = values;
|
||||
logImEXEvent("job_convert_dollar_to_labor");
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const existingAdjustments = await client.query({
|
||||
query: QUERY_JOB_LBR_ADJUSTMENTS,
|
||||
variables: {
|
||||
id: job.id,
|
||||
},
|
||||
});
|
||||
|
||||
const newAdjustments = _.cloneDeep(
|
||||
existingAdjustments.data.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
const adjustment = calculateAdjustment({ mod_lbr_ty, job, jobline });
|
||||
newAdjustments[mod_lbr_ty] = (newAdjustments[mod_lbr_ty] || 0) + adjustment;
|
||||
|
||||
const jobUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB,
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: { lbr_adjustments: newAdjustments },
|
||||
},
|
||||
});
|
||||
|
||||
const lineUpdate = client.mutate({
|
||||
mutation: UPDATE_JOB_LINE,
|
||||
variables: {
|
||||
lineId: jobline.id,
|
||||
line: {
|
||||
convertedtolbr: true,
|
||||
convertedtolbr_data: {
|
||||
mod_lbr_ty: mod_lbr_ty,
|
||||
mod_lb_hrs: adjustment,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!!jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("jobs.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!!lineUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("joblines.errors.saving", {
|
||||
message: JSON.stringify(lineUpdate.errors),
|
||||
}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
hours: calculateAdjustment({ mod_lbr_ty, job, jobline }).toFixed(1),
|
||||
mod_lbr_ty,
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<Card>
|
||||
<Form form={form} layout="vertical" onFinish={handleFinish}>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.mod_lbr_ty")}
|
||||
name="mod_lbr_ty"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select allowClear optionFilterProp="children" showSearch>
|
||||
<Select.Option value="LAA">
|
||||
{t("joblines.fields.lbr_types.LAA")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAB">
|
||||
{t("joblines.fields.lbr_types.LAB")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAD">
|
||||
{t("joblines.fields.lbr_types.LAD")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAE">
|
||||
{t("joblines.fields.lbr_types.LAE")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAF">
|
||||
{t("joblines.fields.lbr_types.LAF")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAG">
|
||||
{t("joblines.fields.lbr_types.LAG")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAM">
|
||||
{t("joblines.fields.lbr_types.LAM")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAR">
|
||||
{t("joblines.fields.lbr_types.LAR")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAS">
|
||||
{t("joblines.fields.lbr_types.LAS")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LAU">
|
||||
{t("joblines.fields.lbr_types.LAU")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA1">
|
||||
{t("joblines.fields.lbr_types.LA1")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA2">
|
||||
{t("joblines.fields.lbr_types.LA2")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA3">
|
||||
{t("joblines.fields.lbr_types.LA3")}
|
||||
</Select.Option>
|
||||
<Select.Option value="LA4">
|
||||
{t("joblines.fields.lbr_types.LA4")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const { mod_lbr_ty } = form.getFieldsValue();
|
||||
return t("joblines.labels.adjustmenttobeadded", {
|
||||
adjustment: calculateAdjustment({
|
||||
mod_lbr_ty,
|
||||
job,
|
||||
jobline,
|
||||
}).toFixed(1),
|
||||
});
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={jobline.convertedtolbr}
|
||||
htmlType="submit"
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisibility(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const handleClick = (e) => {
|
||||
setLoading(true);
|
||||
|
||||
form.setFieldsValue({
|
||||
// date: new moment(),
|
||||
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
// painthrs: Math.round(v.painthrs * 10) / 10,
|
||||
});
|
||||
setVisibility(true);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{jobline.act_price !== 0 && (
|
||||
<Popover
|
||||
disabled={jobline.convertedtolbr}
|
||||
content={overlay}
|
||||
visible={visibility}
|
||||
placement="bottom"
|
||||
>
|
||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||
<Button
|
||||
type="link"
|
||||
disabled={jobline.convertedtolbr}
|
||||
loading={loading}
|
||||
onClick={handleClick}
|
||||
{...otherBtnProps}
|
||||
>
|
||||
<ClockCircleOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function calculateAdjustment({ mod_lbr_ty, job, jobline }) {
|
||||
if (!mod_lbr_ty) return 0;
|
||||
const rate = job[`rate_${mod_lbr_ty.toLowerCase()}`];
|
||||
|
||||
if (rate === 0 || rate === null || rate === undefined) return 0;
|
||||
const adj = jobline.act_price / job[`rate_${mod_lbr_ty.toLowerCase()}`];
|
||||
|
||||
return adj;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Form, notification } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
@@ -37,6 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
setLoading(true);
|
||||
const result = await updateJob({
|
||||
variables: { jobId: job.id, job: values },
|
||||
refetchQueries: ['GET_JOB_BY_PK'],
|
||||
awaitRefetchQueries:true
|
||||
});
|
||||
|
||||
const changedAuditFields = form.getFieldsValue(
|
||||
@@ -65,6 +68,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
}),
|
||||
});
|
||||
}
|
||||
form.resetFields();
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
//Get the owner details, populate it all back into the job.
|
||||
};
|
||||
@@ -87,6 +92,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
|
||||
: null,
|
||||
}}
|
||||
>
|
||||
<FormFieldsChanged form={form} />
|
||||
<LayoutFormRow header={t("jobs.forms.estdates")}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.date_estimated")}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Form, Select } from "antd";
|
||||
import { Form, Select, Space, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -8,6 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
||||
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
||||
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
||||
import { WarningOutlined } from "@ant-design/icons";
|
||||
import "./jobs-close-lines.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -62,14 +63,23 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
</Form.Item>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
span={2}
|
||||
// label={t("joblines.fields.act_price")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<ReadOnlyFormItem type="currency" />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Form.Item
|
||||
span={2}
|
||||
// label={t("joblines.fields.act_price")}
|
||||
key={`${index}act_price`}
|
||||
name={[field.name, "act_price"]}
|
||||
>
|
||||
<ReadOnlyFormItem type="currency" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
noStyle
|
||||
key={`${index}convertedtolbr`}
|
||||
name={[field.name, "convertedtolbr"]}
|
||||
>
|
||||
<HasBeenConvertedTolabor />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Item
|
||||
@@ -192,3 +202,14 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseLines);
|
||||
|
||||
const HasBeenConvertedTolabor = ({ value }) => {
|
||||
const { t } = useTranslation();
|
||||
console.log(value);
|
||||
if (!value) return null;
|
||||
return (
|
||||
<Tooltip title={t("joblines.labels.convertedtolabor")}>
|
||||
<WarningOutlined style={{ color: "tomato" }} />
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Table, Input, Card, Space } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function JobsCreateVehicleInfoSearchComponent({
|
||||
loading,
|
||||
@@ -27,7 +28,9 @@ export default function JobsCreateVehicleInfoSearchComponent({
|
||||
tableState.sortedInfo.columnKey === "v_vin" &&
|
||||
tableState.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/vehicles/" + record.id}>{record.v_vin}</Link>
|
||||
<Link to={"/manage/vehicles/" + record.id}>
|
||||
<VehicleVinDisplay>{record.v_vin}</VehicleVinDisplay>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -180,6 +181,11 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{job.ownr_ea || ""}
|
||||
</DataLabel>
|
||||
{job.owner?.tax_number && (
|
||||
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
||||
{job.owner?.tax_number || ""}
|
||||
</DataLabel>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -211,27 +217,39 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
|
||||
}`})`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||
{`${job.v_vin || t("general.labels.na")}`}
|
||||
<VehicleVinDisplay>
|
||||
{`${job.v_vin || t("general.labels.na")}`}
|
||||
</VehicleVinDisplay>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
<DataLabel label={t("vehicles.fields.notes")}>
|
||||
<span style={{ whiteSpace: "pre" }}>{job.vehicle.notes}</span>
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{job.vehicle.notes}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.v_paint_codes", { number: "" })}
|
||||
>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
.filter(
|
||||
(key) =>
|
||||
job.vehicle.v_paint_codes[key] !== "" &&
|
||||
job.vehicle.v_paint_codes[key] !== null &&
|
||||
job.vehicle.v_paint_codes[key] !== undefined
|
||||
)
|
||||
.map((key, idx) => (
|
||||
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||
))}
|
||||
</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>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -25,6 +25,7 @@ function JobsDocumentGalleryExternal({
|
||||
id: value.id,
|
||||
type: value.type,
|
||||
tags: [{ value: value.type, title: value.type }],
|
||||
size: value.size,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download";
|
||||
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
|
||||
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
|
||||
|
||||
@@ -78,6 +79,7 @@ export function JobsDocumentsLocalGallery({
|
||||
</a>
|
||||
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
|
||||
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
|
||||
<JobsLocalGalleryDownloadButton job={job} />
|
||||
</Space>
|
||||
<Card>
|
||||
<DocumentsLocalUploadComponent
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Button } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import cleanAxios from "../../utils/CleanAxios";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectAllMedia } from "../../redux/media/media.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
allMedia: selectAllMedia,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobsLocalGalleryDownloadButton);
|
||||
|
||||
export function JobsLocalGalleryDownloadButton({
|
||||
bodyshop,
|
||||
galleryImages,
|
||||
allMedia,
|
||||
job,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [download, setDownload] = useState(null);
|
||||
|
||||
function downloadProgress(progressEvent) {
|
||||
setDownload((currentDownloadState) => {
|
||||
return {
|
||||
downloaded: progressEvent.loaded || 0,
|
||||
speed:
|
||||
(progressEvent.loaded || 0) -
|
||||
((currentDownloadState && currentDownloadState.downloaded) || 0),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const handleDownload = async () => {
|
||||
const theDownloadedZip = await cleanAxios.post(
|
||||
`${bodyshop.localmediaserverhttp}/jobs/download`,
|
||||
{
|
||||
jobid: job.id,
|
||||
files: ((allMedia && allMedia[job.id]) || [])
|
||||
.filter((i) => i.isSelected)
|
||||
.map((i) => i.filename),
|
||||
},
|
||||
{
|
||||
headers: { ims_token: bodyshop.localmediatoken },
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: downloadProgress,
|
||||
}
|
||||
);
|
||||
setDownload(null);
|
||||
standardMediaDownload(theDownloadedZip.data, job.ro_number);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button loading={!!download} onClick={handleDownload}>
|
||||
{t("documents.actions.download")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function standardMediaDownload(bufferData, filename) {
|
||||
const a = document.createElement("a");
|
||||
const url = window.URL.createObjectURL(new Blob([bufferData]));
|
||||
a.href = url;
|
||||
a.download = `${filename}.zip`;
|
||||
a.click();
|
||||
}
|
||||
@@ -77,6 +77,12 @@ export function JobsList({ bodyshop }) {
|
||||
(j.v_model_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(j.est_ct_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(j.est_ct_ln || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(j.v_make_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
@@ -264,6 +270,32 @@ export function JobsList({ bodyshop }) {
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.estimator"),
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
filterSearch: true,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "N/A",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(
|
||||
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
|
||||
),
|
||||
render: (text, record) =>
|
||||
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.comment"),
|
||||
dataIndex: "comment",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
@@ -40,6 +41,8 @@ export function JobNotesComponent({
|
||||
relatedRos,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [filter, setFilter] = useLocalStorage("filter_job_notes_icons", null);
|
||||
|
||||
const Templates = TemplateList("job_special", {
|
||||
ro_number,
|
||||
});
|
||||
@@ -50,6 +53,18 @@ export function JobNotesComponent({
|
||||
dataIndex: "icons",
|
||||
key: "icons",
|
||||
width: 80,
|
||||
filteredValue: filter?.icons || null,
|
||||
filters: [
|
||||
{
|
||||
text: t("notes.labels.usernotes"),
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
text: t("notes.labels.systemnotes"),
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => record.audit === value,
|
||||
render: (text, record) => (
|
||||
<span>
|
||||
{record.critical ? (
|
||||
@@ -131,6 +146,10 @@ export function JobNotesComponent({
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setFilter(filters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LayoutFormRow>
|
||||
@@ -166,6 +185,7 @@ export function JobNotesComponent({
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={data}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +89,12 @@ export function JobsReadyList({ bodyshop }) {
|
||||
(j.v_model_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(j.est_ct_fn || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(j.est_ct_ln || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()) ||
|
||||
(j.v_make_desc || "")
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase())
|
||||
@@ -276,6 +282,32 @@ export function JobsReadyList({ bodyshop }) {
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.estimator"),
|
||||
dataIndex: "jobs.labels.estimator",
|
||||
key: "jobs.labels.estimator",
|
||||
ellipsis: true,
|
||||
responsive: ["xl"],
|
||||
filterSearch: true,
|
||||
filters:
|
||||
(jobs &&
|
||||
jobs
|
||||
.map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim())
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "N/A",
|
||||
value: [s],
|
||||
};
|
||||
})) ||
|
||||
[],
|
||||
onFilter: (value, record) =>
|
||||
value.includes(
|
||||
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim()
|
||||
),
|
||||
render: (text, record) =>
|
||||
`${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(),
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.comment"),
|
||||
dataIndex: "comment",
|
||||
|
||||
@@ -12,7 +12,24 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
|
||||
export default function LaborAllocationsAdjustmentEdit({
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation }) =>
|
||||
dispatch(insertAuditTrail({ jobid, operation })),
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LaborAllocationsAdjustmentEdit);
|
||||
|
||||
export function LaborAllocationsAdjustmentEdit({
|
||||
insertAuditTrail,
|
||||
jobId,
|
||||
mod_lbr_ty,
|
||||
adjustments,
|
||||
@@ -51,6 +68,15 @@ export default function LaborAllocationsAdjustmentEdit({
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.save"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: AuditTrailMapping.jobmodifylbradj({
|
||||
mod_lbr_ty: values.mod_lbr_ty,
|
||||
hours:
|
||||
values.hours -
|
||||
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
|
||||
}),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { EditFilled } from "@ant-design/icons";
|
||||
import { Card, Space, Table } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Card, Col, Row, Space, Table } from "antd";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component";
|
||||
import "./labor-allocations-table.styles.scss";
|
||||
import { CalculateAllocationsTotals } from "./labor-allocations-table.utility";
|
||||
import _ from "lodash";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician,
|
||||
@@ -43,6 +45,11 @@ export function LaborAllocationsTable({
|
||||
if (!jobId) setTotals([]);
|
||||
}, [joblines, timetickets, bodyshop, adjustments, jobId]);
|
||||
|
||||
const convertedLines = useMemo(
|
||||
() => joblines && joblines.filter((j) => j.convertedtolbr),
|
||||
[joblines]
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("timetickets.fields.cost_center"),
|
||||
@@ -114,24 +121,105 @@ export function LaborAllocationsTable({
|
||||
),
|
||||
},
|
||||
];
|
||||
const convertedTableCols = [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.op_code_desc"),
|
||||
dataIndex: "op_code_desc",
|
||||
key: "op_code_desc",
|
||||
ellipsis: true,
|
||||
render: (text, record) =>
|
||||
`${record.op_code_desc || ""}${
|
||||
record.alt_partm ? ` ${record.alt_partm}` : ""
|
||||
}`,
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
ellipsis: true,
|
||||
render: (text, record) => (
|
||||
<>
|
||||
<CurrencyFormatter>
|
||||
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||
? record.prt_dsmk_m
|
||||
: record.act_price}
|
||||
</CurrencyFormatter>
|
||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||
<span
|
||||
style={{ marginLeft: ".2rem" }}
|
||||
>{`(${record.prt_dsmk_p}%)`}</span>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.part_qty"),
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty",
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.mod_lbr_ty"),
|
||||
dataIndex: "conv_mod_lbr_ty",
|
||||
key: "conv_mod_lbr_ty",
|
||||
render: (text, record) =>
|
||||
record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.mod_lb_hrs"),
|
||||
dataIndex: "conv_mod_lb_hrs",
|
||||
key: "conv_mod_lb_hrs",
|
||||
render: (text, record) =>
|
||||
record.convertedtolbr_data &&
|
||||
record.convertedtolbr_data.mod_lb_hrs &&
|
||||
record.convertedtolbr_data.mod_lb_hrs.toFixed(1),
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.laborallocations")}>
|
||||
<Table
|
||||
columns={columns}
|
||||
rowKey="cost_center"
|
||||
pagination={false}
|
||||
onChange={handleTableChange}
|
||||
dataSource={totals}
|
||||
scroll={{
|
||||
x: true,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.laborallocations")}>
|
||||
<Table
|
||||
columns={columns}
|
||||
rowKey="cost_center"
|
||||
pagination={false}
|
||||
onChange={handleTableChange}
|
||||
dataSource={totals}
|
||||
scroll={{
|
||||
x: true,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
{convertedLines && convertedLines.length > 0 && (
|
||||
<Col span={24}>
|
||||
<Card title={t("jobs.labels.convertedtolabor")}>
|
||||
<Table
|
||||
columns={convertedTableCols}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
dataSource={convertedLines}
|
||||
scroll={{
|
||||
x: true,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, null)(LaborAllocationsTable);
|
||||
|
||||
@@ -94,6 +94,12 @@ export default function OwnerDetailFormComponent({ form, loading }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("owners.fields.tax_number")}
|
||||
name="tax_number"
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Form.Item label={t("owners.fields.note")} name="note">
|
||||
<Input.TextArea rows={4} />
|
||||
|
||||
@@ -20,9 +20,10 @@ function OwnerDetailFormContainer({ owner, refetch }) {
|
||||
if (!!result.errors) {
|
||||
notification["error"]({
|
||||
message: t("owners.errors.saving", {
|
||||
message: JSON.stringify(result.errors),
|
||||
error: JSON.stringify(result.errors),
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function OwnerFindModalContainer({
|
||||
|
||||
useEffect(() => {
|
||||
if (modalProps.visible && owner) {
|
||||
const s = OwnerNameDisplayFunction(owner);
|
||||
const s = OwnerNameDisplayFunction(owner, true);
|
||||
|
||||
setSearchText(s.trim());
|
||||
callSearchowners({ variables: { search: s.trim() } });
|
||||
|
||||
@@ -27,7 +27,7 @@ export function OwnerNameDisplay({ bodyshop, ownerObject }) {
|
||||
}`.trim();
|
||||
}
|
||||
|
||||
export function OwnerNameDisplayFunction(ownerObject) {
|
||||
export function OwnerNameDisplayFunction(ownerObject, forceFirstLast = false) {
|
||||
const emptyTest =
|
||||
ownerObject.ownr_fn + ownerObject.ownr_ln + ownerObject.ownr_co_nm;
|
||||
|
||||
@@ -36,7 +36,7 @@ export function OwnerNameDisplayFunction(ownerObject) {
|
||||
|
||||
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 || ""} ${
|
||||
ownerObject.ownr_co_nm || ""
|
||||
}`.trim();
|
||||
|
||||
@@ -124,6 +124,15 @@ export function PartsOrderModalComponent({
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
<Form.Item
|
||||
name="is_quote"
|
||||
label={t("parts_orders.labels.is_quote")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<Divider orientation="left">
|
||||
{t("parts_orders.labels.inthisorder")}
|
||||
@@ -291,17 +300,32 @@ export function PartsOrderModalComponent({
|
||||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
|
||||
<Radio.Group
|
||||
defaultValue={sendType}
|
||||
onChange={(e) => setSendType(e.target.value)}
|
||||
>
|
||||
<Radio value={"none"}>{t("general.labels.none")}</Radio>
|
||||
<Radio value={"e"}>{t("parts_orders.labels.email")}</Radio>
|
||||
<Radio value={"p"}>{t("parts_orders.labels.print")}</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{() => {
|
||||
const is_quote = form.getFieldValue("is_quote");
|
||||
if (is_quote) setSendType("oec");
|
||||
return (
|
||||
<Radio.Group
|
||||
defaultValue={sendType}
|
||||
value={sendType}
|
||||
onChange={(e) => setSendType(e.target.value)}
|
||||
>
|
||||
<Radio disabled={is_quote} value={"none"}>
|
||||
{t("general.labels.none")}
|
||||
</Radio>
|
||||
<Radio disabled={is_quote} value={"e"}>
|
||||
{t("parts_orders.labels.email")}
|
||||
</Radio>
|
||||
<Radio disabled={is_quote} value={"p"}>
|
||||
{t("parts_orders.labels.print")}
|
||||
</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,32 +93,56 @@ export function PartsOrderModalContainer({
|
||||
const [updateJobLines] = useMutation(UPDATE_JOB_LINE_STATUS);
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
|
||||
const handleFinish = async ({ removefrompartsqueue, ...values }) => {
|
||||
const handleFinish = async ({
|
||||
removefrompartsqueue,
|
||||
is_quote,
|
||||
...values
|
||||
}) => {
|
||||
logImEXEvent("parts_order_insert");
|
||||
setSaving(true);
|
||||
const insertResult = await insertPartOrder({
|
||||
variables: {
|
||||
po: [
|
||||
{
|
||||
...values,
|
||||
order_date: moment().format("YYYY-MM-DD"),
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobId,
|
||||
user_email: currentUser.email,
|
||||
return: isReturn,
|
||||
status: bodyshop.md_order_statuses.default_ordered || "Ordered*",
|
||||
},
|
||||
],
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||
});
|
||||
if (!!insertResult.error) {
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.creating"),
|
||||
description: JSON.stringify(insertResult.error),
|
||||
let insertResult;
|
||||
|
||||
insertResult = await insertPartOrder({
|
||||
variables: {
|
||||
po: [
|
||||
{
|
||||
...values,
|
||||
order_date: moment().format("YYYY-MM-DD"),
|
||||
orderedby: currentUser.email,
|
||||
jobid: jobId,
|
||||
user_email: currentUser.email,
|
||||
return: isReturn,
|
||||
status: is_quote
|
||||
? bodyshop.md_order_statuses.default_quote || "Quote"
|
||||
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
|
||||
},
|
||||
],
|
||||
},
|
||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!!insertResult.errors) {
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.creating"),
|
||||
description: JSON.stringify(insertResult.errors),
|
||||
});
|
||||
return;
|
||||
}
|
||||
notification["success"]({
|
||||
message: values.isReturn
|
||||
? t("parts_orders.successes.return_created")
|
||||
: t("parts_orders.successes.created"),
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: isReturn
|
||||
? AuditTrailMapping.jobspartsreturn(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
)
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
const jobLinesResult = await updateJobLines({
|
||||
variables: {
|
||||
@@ -127,6 +151,8 @@ export function PartsOrderModalContainer({
|
||||
.map((item) => item.job_line_id),
|
||||
status: isReturn
|
||||
? bodyshop.md_order_statuses.default_returned || "Returned*"
|
||||
: is_quote
|
||||
? bodyshop.md_order_statuses.default_quote || "Quote"
|
||||
: bodyshop.md_order_statuses.default_ordered || "Ordered*",
|
||||
},
|
||||
});
|
||||
@@ -142,17 +168,6 @@ export function PartsOrderModalContainer({
|
||||
});
|
||||
}
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: jobId,
|
||||
operation: isReturn
|
||||
? AuditTrailMapping.jobspartsreturn(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
)
|
||||
: AuditTrailMapping.jobspartsorder(
|
||||
insertResult.data.insert_parts_orders.returning[0].order_number
|
||||
),
|
||||
});
|
||||
|
||||
if (!!jobLinesResult.errors) {
|
||||
notification["error"]({
|
||||
message: t("parts_orders.errors.creating"),
|
||||
@@ -160,12 +175,6 @@ export function PartsOrderModalContainer({
|
||||
});
|
||||
}
|
||||
|
||||
notification["success"]({
|
||||
message: values.isReturn
|
||||
? t("parts_orders.successes.return_created")
|
||||
: t("parts_orders.successes.created"),
|
||||
});
|
||||
|
||||
if (values.vendorid === bodyshop.inhousevendorid) {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
|
||||
@@ -2,7 +2,6 @@ import { PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Space } from "antd";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { TimeFormatter } from "../../utils/DateFormatter";
|
||||
@@ -530,6 +529,27 @@ const r = ({ technician, state, activeStatuses, bodyshop }) => {
|
||||
<JobPartsQueueCount parts={record.joblines_status} record={record} />
|
||||
),
|
||||
},
|
||||
//Added as a place holder for St Claude. Not implemented as it requires another join for a field used by only 1 client.
|
||||
// {
|
||||
// title: i18n.t("vehicles.fields.v_paint_codes", { number: "" }),
|
||||
// dataIndex: "v_paint_codes",
|
||||
// key: "v_paint_codes",
|
||||
// render: (text, record) =>
|
||||
// record.vehicle?.v_paint_codes ? (
|
||||
// <span style={{ whiteSpace: "pre" }}>
|
||||
// {Object.keys(record.vehicle.v_paint_codes)
|
||||
// .filter(
|
||||
// (key) =>
|
||||
// record.vehicle.v_paint_codes[key] !== "" &&
|
||||
// record.vehicle.v_paint_codes[key] !== null &&
|
||||
// record.vehicle.v_paint_codes[key] !== undefined
|
||||
// )
|
||||
// .map((key, idx) => (
|
||||
// <Tag key={idx}>{record.vehicle.v_paint_codes[key]}</Tag>
|
||||
// ))}
|
||||
// </span>
|
||||
// ) : null,
|
||||
// },
|
||||
];
|
||||
};
|
||||
export default r;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import ScheduleBlockDay from "../schedule-block-day/schedule-block-day.component";
|
||||
import ScheduleCalendarHeaderGraph from "./schedule-calendar-header-graph.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -38,7 +39,9 @@ export function ScheduleCalendarHeaderComponent({
|
||||
const ATSToday = useMemo(() => {
|
||||
if (!events) return [];
|
||||
return _.groupBy(
|
||||
events.filter((e) => moment(date).isSame(moment(e.start), "day")),
|
||||
events.filter(
|
||||
(e) => !e.vacation && moment(date).isSame(moment(e.start), "day")
|
||||
),
|
||||
"job.alt_transport"
|
||||
);
|
||||
}, [events, date]);
|
||||
@@ -66,6 +69,9 @@ export function ScheduleCalendarHeaderComponent({
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>
|
||||
{`(${(
|
||||
j.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
@@ -99,6 +105,9 @@ export function ScheduleCalendarHeaderComponent({
|
||||
<td>
|
||||
<Link to={`/manage/jobs/${j.id}`}>{j.ro_number}</Link>
|
||||
</td>
|
||||
<td>
|
||||
<OwnerNameDisplay ownerObject={j} />
|
||||
</td>
|
||||
<td>
|
||||
{`(${(
|
||||
j.labhrs.aggregate.sum.mod_lb_hrs +
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
.rbc-time-view .rbc-allday-cell {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.rbc-month-row{
|
||||
overflow: unset !important;
|
||||
}
|
||||
// .rbc-row-content {
|
||||
// display: none;
|
||||
// }
|
||||
|
||||
@@ -34,7 +34,7 @@ export function ScheduleCalendarWrapperComponent({
|
||||
const { t } = useTranslation();
|
||||
const handleEventPropStyles = (event, start, end, isSelected) => {
|
||||
return {
|
||||
...(event.color
|
||||
...(event.color && !((search.view || defaultView) === "agenda")
|
||||
? {
|
||||
style: {
|
||||
backgroundColor:
|
||||
|
||||
@@ -80,6 +80,7 @@ export default function ScoreboardTimeTickets() {
|
||||
totalThisMonth: 0,
|
||||
totalLastMonth: 0,
|
||||
totalOverPeriod: 0,
|
||||
actualTotalOverPeriod: 0,
|
||||
employees: {},
|
||||
};
|
||||
data.fixedperiod.forEach((ticket) => {
|
||||
@@ -92,6 +93,7 @@ export default function ScoreboardTimeTickets() {
|
||||
totalThisMonth: 0,
|
||||
totalLastMonth: 0,
|
||||
totalOverPeriod: 0,
|
||||
actualTotalOverPeriod: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,6 +184,9 @@ export default function ScoreboardTimeTickets() {
|
||||
ret.employees[ticket.employee.employee_number].totalOverPeriod =
|
||||
ret.employees[ticket.employee.employee_number].totalOverPeriod +
|
||||
ticket.productivehrs;
|
||||
ret.employees[ticket.employee.employee_number].actualTotalOverPeriod =
|
||||
ret.employees[ticket.employee.employee_number].actualTotalOverPeriod +
|
||||
(ticket.actualhrs || 0);
|
||||
|
||||
if (!totals.employees[ticket.employee.employee_number])
|
||||
totals.employees[ticket.employee.employee_number] = {
|
||||
@@ -219,7 +224,7 @@ export default function ScoreboardTimeTickets() {
|
||||
roundObject(ret);
|
||||
roundObject(totals);
|
||||
roundObject(ret2);
|
||||
console.log(ret);
|
||||
|
||||
return {
|
||||
fixed: ret,
|
||||
timeperiod: {
|
||||
@@ -231,6 +236,8 @@ export default function ScoreboardTimeTickets() {
|
||||
};
|
||||
}, [fixedPeriods, data, startDate, endDate]);
|
||||
|
||||
console.log(calculatedData);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (loading) return <LoadingSpinner />;
|
||||
return (
|
||||
|
||||
@@ -19,7 +19,6 @@ export default connect(
|
||||
|
||||
export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
console.log(data);
|
||||
const columns = [
|
||||
{
|
||||
title: t("employees.fields.employee_number"),
|
||||
@@ -57,6 +56,16 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
key: "totalOverPeriod",
|
||||
sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod,
|
||||
},
|
||||
{
|
||||
title: t("scoreboard.labels.efficiencyoverperiod"),
|
||||
dataIndex: "efficiencyoverperiod",
|
||||
key: "efficiencyoverperiod",
|
||||
render: (text, record) =>
|
||||
`${(
|
||||
(record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) *
|
||||
100
|
||||
).toFixed(1)} %`,
|
||||
},
|
||||
];
|
||||
|
||||
const tableData = data
|
||||
@@ -112,6 +121,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
|
||||
<Col md={24} lg={20}>
|
||||
<Table
|
||||
columns={columns}
|
||||
rowKey='employee_number'
|
||||
dataSource={tableData}
|
||||
id="employee_number"
|
||||
scroll={{ y: "300px" }}
|
||||
|
||||
@@ -43,9 +43,10 @@ export default function ShopEmployeeAddVacation({ employee }) {
|
||||
});
|
||||
} else {
|
||||
notification["success"]({
|
||||
message: t("employees.successes.added"),
|
||||
message: t("employees.successes.vacationadded"),
|
||||
});
|
||||
}
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
@@ -1494,7 +1494,7 @@ export default function ShopInfoGeneral({ form }) {
|
||||
}
|
||||
|
||||
const ReceivableCustomFieldSelect = (
|
||||
<Select>
|
||||
<Select allowClear>
|
||||
<Select.Option value="v_vin">VIN</Select.Option>
|
||||
<Select.Option value="clm_no">Claim No.</Select.Option>
|
||||
<Select.Option value="ded_amt">Deductible Amount</Select.Option>
|
||||
|
||||
@@ -2,9 +2,28 @@ import { Form, Input } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
export default function ShopInfoOrderStatusComponent({ form }) {
|
||||
const { t } = useTranslation();
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ShopInfoOrderStatusComponent);
|
||||
|
||||
export function ShopInfoOrderStatusComponent({ bodyshop, form }) {
|
||||
const { t } = useTranslation();
|
||||
const { OEConnection } = useTreatments(
|
||||
["OEConnection"],
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
return (
|
||||
<LayoutFormRow header={t("bodyshop.labels.orderstatuses")}>
|
||||
<Form.Item
|
||||
@@ -56,6 +75,20 @@ export default function ShopInfoOrderStatusComponent({ form }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{OEConnection.treatment === "on" && (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.statuses.default_quote")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_order_statuses", "default_quote"]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +146,8 @@ const JobRelatedTicketsTable = ({
|
||||
title: t("employees.labels.name"),
|
||||
dataIndex: "empname",
|
||||
key: "empname",
|
||||
sorter: (a, b) => alphaSort(a.empname, b.empname),
|
||||
sorter: (a, b) =>
|
||||
alphaSort(a.item.employee.last_name, b.item.employee.last_name),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "empname" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
@@ -172,7 +173,9 @@ const JobRelatedTicketsTable = ({
|
||||
title: t("timetickets.fields.efficiency"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
sorter: (a, b) => a.total - b.total,
|
||||
sorter: (a, b) =>
|
||||
(a.actHrs === 0 || !a.actHrs ? 0 : (a.prodHrs / a.actHrs) * 100) -
|
||||
(b.actHrs === 0 || !b.actHrs ? 0 : (b.prodHrs / b.actHrs) * 100),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) =>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, Col, Descriptions, Popover, Row, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function VehicleTagPopoverComponent({ job }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,7 +28,9 @@ export default function VehicleTagPopoverComponent({ job }) {
|
||||
{`${job.plate_st || t("general.labels.na")}`}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="4" label={t("vehicles.fields.v_vin")}>
|
||||
{`${job.v_vin || t("general.labels.na")}`}
|
||||
<VehicleVinDisplay>
|
||||
{`${job.v_vin || t("general.labels.na")}`}
|
||||
</VehicleVinDisplay>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Col>
|
||||
@@ -46,7 +49,9 @@ export default function VehicleTagPopoverComponent({ job }) {
|
||||
{`${job.vehicle.plate_st || t("general.labels.na")}`}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item key="4" label={t("vehicles.fields.v_vin")}>
|
||||
{`${job.vehicle.v_vin || t("general.labels.na")}`}
|
||||
<VehicleVinDisplay>{`${
|
||||
job.vehicle.v_vin || t("general.labels.na")
|
||||
}`}</VehicleVinDisplay>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Col>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
export default function VehicleVinDisplay({ children }) {
|
||||
if (!children) return null;
|
||||
|
||||
if (typeof children !== "string" || children.length !== 17) return children;
|
||||
const vin = children.trim();
|
||||
|
||||
const first = vin.substring(0, 9);
|
||||
const second = vin.substring(9, 17);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{first}</span>
|
||||
<span style={{ textDecoration: "underline" }}>{second}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
export default function VehiclesListComponent({
|
||||
loading,
|
||||
vehicles,
|
||||
@@ -31,7 +32,7 @@ export default function VehiclesListComponent({
|
||||
key: "v_vin",
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/vehicles/" + record.id}>
|
||||
{record.v_vin || "N/A"}
|
||||
<VehicleVinDisplay>{record.v_vin || "N/A"}</VehicleVinDisplay>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,75 +1,77 @@
|
||||
import { gql } from "@apollo/client";
|
||||
|
||||
export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
||||
query QUERY_ALL_ACTIVE_APPOINTMENTS(
|
||||
$start: timestamptz!
|
||||
$end: timestamptz!
|
||||
$startd: date!
|
||||
$endd: date!
|
||||
query QUERY_ALL_ACTIVE_APPOINTMENTS(
|
||||
$start: timestamptz!
|
||||
$end: timestamptz!
|
||||
$startd: date!
|
||||
$endd: date!
|
||||
) {
|
||||
employee_vacation(
|
||||
where: { _or: [{ start: { _gte: $startd } },
|
||||
{ end: { _lte: $endd } },
|
||||
{_and:[{start:{_lte: $startd}},{end:{_gte:$endd}}]}] }
|
||||
) {
|
||||
employee_vacation(
|
||||
where: { _or: [{ start: { _gte: $startd } }, { end: { _lte: $endd } }] }
|
||||
) {
|
||||
id
|
||||
start
|
||||
end
|
||||
employee {
|
||||
id
|
||||
start
|
||||
end
|
||||
employee {
|
||||
id
|
||||
last_name
|
||||
first_name
|
||||
last_name
|
||||
first_name
|
||||
}
|
||||
}
|
||||
appointments(
|
||||
where: {
|
||||
canceled: { _eq: false }
|
||||
end: { _lte: $end }
|
||||
start: { _gte: $start }
|
||||
}
|
||||
) {
|
||||
start
|
||||
id
|
||||
end
|
||||
arrived
|
||||
title
|
||||
isintake
|
||||
block
|
||||
color
|
||||
note
|
||||
job {
|
||||
alt_transport
|
||||
ro_number
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ea
|
||||
clm_total
|
||||
id
|
||||
clm_no
|
||||
ins_co_nm
|
||||
v_model_yr
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
appointments(
|
||||
where: {
|
||||
canceled: { _eq: false }
|
||||
end: { _lte: $end }
|
||||
start: { _gte: $start }
|
||||
}
|
||||
) {
|
||||
start
|
||||
id
|
||||
end
|
||||
arrived
|
||||
title
|
||||
isintake
|
||||
block
|
||||
color
|
||||
note
|
||||
job {
|
||||
alt_transport
|
||||
ro_number
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_fn
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ownr_ea
|
||||
clm_total
|
||||
id
|
||||
clm_no
|
||||
ins_co_nm
|
||||
v_model_yr
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
larhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _eq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
aggregate {
|
||||
sum {
|
||||
mod_lb_hrs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -324,6 +326,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
ro_number
|
||||
scheduled_completion
|
||||
actual_completion
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
@@ -352,6 +357,9 @@ export const QUERY_SCHEDULE_LOAD_DATA = gql`
|
||||
id
|
||||
scheduled_in
|
||||
ro_number
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
labhrs: joblines_aggregate(
|
||||
where: { mod_lbr_ty: { _neq: "LAR" }, removed: { _eq: false } }
|
||||
) {
|
||||
|
||||
@@ -48,6 +48,8 @@ export const GET_LINE_TICKET_BY_PK = gql`
|
||||
lbr_op
|
||||
lbr_amt
|
||||
op_code_desc
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
}
|
||||
timetickets(where: { jobid: { _eq: $id } }) {
|
||||
actualhrs
|
||||
@@ -177,6 +179,8 @@ export const UPDATE_JOB_LINE = gql`
|
||||
location
|
||||
status
|
||||
removed
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
|
||||
updated_at
|
||||
ded_amt
|
||||
suspended
|
||||
est_ct_fn
|
||||
est_ct_ln
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -621,6 +623,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
ownr_ctry
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
tax_number
|
||||
}
|
||||
labor_rate_desc
|
||||
rate_la1
|
||||
@@ -684,6 +687,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
line_ref
|
||||
part_type
|
||||
oem_partno
|
||||
alt_partno
|
||||
db_price
|
||||
act_price
|
||||
part_qty
|
||||
@@ -702,6 +706,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
prt_dsmk_p
|
||||
prt_dsmk_m
|
||||
ioucreated
|
||||
convertedtolbr
|
||||
billlines(limit: 1, order_by: { bill: { date: desc } }) {
|
||||
id
|
||||
quantity
|
||||
@@ -854,6 +859,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
id
|
||||
allow_text_message
|
||||
preferred_contact
|
||||
tax_number
|
||||
}
|
||||
vehicleid
|
||||
v_model_yr
|
||||
@@ -1905,6 +1911,8 @@ export const QUERY_JOB_CLOSE_DETAILS = gql`
|
||||
profitcenter_labor
|
||||
profitcenter_part
|
||||
prt_dsmk_p
|
||||
convertedtolbr
|
||||
convertedtolbr_data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export const QUERY_OWNER_BY_ID = gql`
|
||||
ownr_zip
|
||||
preferred_contact
|
||||
note
|
||||
tax_number
|
||||
jobs {
|
||||
id
|
||||
ro_number
|
||||
|
||||
@@ -79,6 +79,7 @@ export const QUERY_TIME_TICKETS_IN_RANGE_SB = gql`
|
||||
date
|
||||
id
|
||||
rate
|
||||
actualhrs
|
||||
productivehrs
|
||||
memo
|
||||
jobid
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
selectBodyshop,
|
||||
selectInstanceConflict,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import * as Sentry from "@sentry/react";
|
||||
|
||||
import "./manage.page.styles.scss";
|
||||
|
||||
const ManageRootPage = lazy(() =>
|
||||
@@ -407,7 +409,10 @@ export function Manage({ match, conflict, bodyshop }) {
|
||||
|
||||
<Content className="content-container">
|
||||
<PartnerPingComponent />
|
||||
<ErrorBoundary>{PageContent}</ErrorBoundary>
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundary />} showDialog>
|
||||
{PageContent}
|
||||
</Sentry.ErrorBoundary>
|
||||
|
||||
<BackTop />
|
||||
<Footer>
|
||||
<div
|
||||
|
||||
@@ -17,6 +17,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { onlyUnique } from "../../utils/arrayHelper";
|
||||
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -31,6 +32,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
statusFilters,
|
||||
} = searchParams;
|
||||
const history = useHistory();
|
||||
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_PARTS_QUEUE, {
|
||||
fetchPolicy: "network-only",
|
||||
@@ -92,7 +94,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
// searchParams.page = pagination.current;
|
||||
searchParams.sortcolumn = sorter.columnKey;
|
||||
searchParams.sortorder = sorter.order;
|
||||
|
||||
setFilter(filters);
|
||||
history.push({ search: queryString.stringify(searchParams) });
|
||||
};
|
||||
|
||||
@@ -247,6 +249,7 @@ export function PartsQueuePageComponent({ bodyshop }) {
|
||||
key: "queued_for_parts",
|
||||
sorter: (a, b) => a.queued_for_parts - b.queued_for_parts,
|
||||
sortOrder: sortcolumn === "queued_for_parts" && sortorder,
|
||||
filteredValue: filter?.queued_for_parts || null,
|
||||
filters: [
|
||||
{
|
||||
text: "Queued",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import axios from "axios";
|
||||
import phone from "phone";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CONVERSATION_ID_BY_PHONE,
|
||||
CREATE_CONVERSATION,
|
||||
CREATE_CONVERSATION
|
||||
} from "../../graphql/conversations.queries";
|
||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
@@ -12,7 +12,7 @@ import { selectBodyshop } from "../user/user.selectors";
|
||||
import {
|
||||
sendMessageFailure,
|
||||
sendMessageSuccess,
|
||||
setSelectedConversation,
|
||||
setSelectedConversation
|
||||
} from "./messaging.actions";
|
||||
import MessagingActionTypes from "./messaging.types";
|
||||
|
||||
@@ -33,13 +33,14 @@ export function* openChatByPhone({ payload }) {
|
||||
logImEXEvent("messaging_open_by_phone");
|
||||
const { phone_num, jobid } = payload;
|
||||
|
||||
const p = parsePhoneNumber(phone_num, "CA");
|
||||
const bodyshop = yield select(selectBodyshop);
|
||||
try {
|
||||
const {
|
||||
data: { conversations },
|
||||
} = yield client.query({
|
||||
query: CONVERSATION_ID_BY_PHONE,
|
||||
variables: { phone: phone(phone_num).phoneNumber },
|
||||
variables: { phone: p.number },
|
||||
});
|
||||
|
||||
if (conversations.length === 0) {
|
||||
@@ -52,7 +53,7 @@ export function* openChatByPhone({ payload }) {
|
||||
variables: {
|
||||
conversation: [
|
||||
{
|
||||
phone_num: phone(phone_num).phoneNumber,
|
||||
phone_num: p.number,
|
||||
bodyshopid: bodyshop.id,
|
||||
job_conversations: jobid ? { data: { jobid: jobid } } : null,
|
||||
},
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
"jobimported": "Job imported.",
|
||||
"jobinproductionchange": "Job production status set to {{inproduction}}",
|
||||
"jobioucreated": "IOU Created.",
|
||||
"jobmodifylbradj": "Labor adjustments modified.",
|
||||
"jobmodifylbradj": "Labor adjustments modified {{mod_lbr_ty}} / {{hours}}.",
|
||||
"jobnoteadded": "Note added to job.",
|
||||
"jobnotedeleted": "Note deleted from job.",
|
||||
"jobnoteupdated": "Note updated on job.",
|
||||
@@ -136,6 +136,9 @@
|
||||
"other": "-- Not On Estimate --",
|
||||
"reconciled": "Reconciled!",
|
||||
"unreconciled": "Unreconciled"
|
||||
},
|
||||
"validation": {
|
||||
"atleastone": "At least one bill line must be entered."
|
||||
}
|
||||
},
|
||||
"bills": {
|
||||
@@ -502,6 +505,7 @@
|
||||
"default_imported": "Default Imported Status",
|
||||
"default_invoiced": "Default Invoiced Status",
|
||||
"default_ordered": "Default Ordered Status",
|
||||
"default_quote": "Default Quote Status",
|
||||
"default_received": "Default Received Status",
|
||||
"default_returned": "Default Returned",
|
||||
"default_scheduled": "Default Scheduled Status",
|
||||
@@ -674,6 +678,7 @@
|
||||
"refuelqty": "Refuel qty.?"
|
||||
},
|
||||
"correctdataonform": "Please review the information above. If any of it is not correct, you can fix it later.",
|
||||
"dateinpast": "Date is in the past.",
|
||||
"dlexpirebeforereturn": "The driver's license expires before the car is expected to return. ",
|
||||
"driverinformation": "Driver's Information",
|
||||
"findcontract": "Find Contract",
|
||||
@@ -748,6 +753,7 @@
|
||||
"status": {
|
||||
"in": "Available",
|
||||
"inservice": "In Service",
|
||||
"leasereturn": "Lease Returned",
|
||||
"out": "Rented",
|
||||
"sold": "Sold"
|
||||
},
|
||||
@@ -916,7 +922,8 @@
|
||||
},
|
||||
"successes": {
|
||||
"delete": "Employee deleted successfully.",
|
||||
"save": "Employee saved successfully."
|
||||
"save": "Employee saved successfully.",
|
||||
"vacationadded": "Employee vacation added."
|
||||
},
|
||||
"validation": {
|
||||
"unique_employee_number": "You must enter a unique employee number."
|
||||
@@ -961,7 +968,8 @@
|
||||
},
|
||||
"errors": {
|
||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||
"notfound": "No record was found."
|
||||
"notfound": "No record was found.",
|
||||
"sizelimit": "The selected items exceed the size limit."
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "CC Contract",
|
||||
@@ -1109,6 +1117,7 @@
|
||||
},
|
||||
"joblines": {
|
||||
"actions": {
|
||||
"converttolabor": "Convert amount to Labor.",
|
||||
"new": "New Line"
|
||||
},
|
||||
"errors": {
|
||||
@@ -1174,7 +1183,9 @@
|
||||
"unq_seq": "Seq #"
|
||||
},
|
||||
"labels": {
|
||||
"adjustmenttobeadded": "Adjustment to be added: {{adjustment}}",
|
||||
"billref": "Latest Bill",
|
||||
"convertedtolabor": "This line has been converted to labor. Ensure you adjust the profit center for the amount accordingly.",
|
||||
"edit": "Edit Line",
|
||||
"ioucreated": "IOU",
|
||||
"new": "New Line",
|
||||
@@ -1562,6 +1573,7 @@
|
||||
"closeconfirm": "Are you sure you want to close this job? This cannot be easily undone.",
|
||||
"closejob": "Close Job {{ro_number}}",
|
||||
"contracts": "CC Contracts",
|
||||
"convertedtolabor": "Lines Converted to Labor",
|
||||
"cost": "Cost",
|
||||
"cost_Additional": "Cost - Additional",
|
||||
"cost_labor": "Cost - Labor",
|
||||
@@ -1604,6 +1616,7 @@
|
||||
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
|
||||
"employeeassignments": "Employee Assignments",
|
||||
"estimatelines": "Estimate Lines",
|
||||
"estimator": "Estimator",
|
||||
"existing_jobs": "Existing Jobs",
|
||||
"federal_tax_amt": "Federal Taxes",
|
||||
"gpdollars": "$ G.P.",
|
||||
@@ -1907,7 +1920,9 @@
|
||||
"labels": {
|
||||
"addtorelatedro": "Add to Related ROs",
|
||||
"newnoteplaceholder": "Add a note...",
|
||||
"notetoadd": "Note to Add"
|
||||
"notetoadd": "Note to Add",
|
||||
"systemnotes": "System Notes",
|
||||
"usernotes": "User Notes"
|
||||
},
|
||||
"successes": {
|
||||
"create": "Note created successfully.",
|
||||
@@ -1926,6 +1941,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"noaccess": "The record does not exist or you do not have access to it. ",
|
||||
"saving": "Error saving owner. {{error}}.",
|
||||
"selectexistingornew": "Select an existing owner record or create a new one. "
|
||||
},
|
||||
"fields": {
|
||||
@@ -1946,7 +1962,8 @@
|
||||
"ownr_st": "Province/State",
|
||||
"ownr_title": "Title",
|
||||
"ownr_zip": "Zip/Postal Code",
|
||||
"preferred_contact": "Preferred Contact Method"
|
||||
"preferred_contact": "Preferred Contact Method",
|
||||
"tax_number": "Tax Number"
|
||||
},
|
||||
"forms": {
|
||||
"address": "Address",
|
||||
@@ -2012,6 +2029,7 @@
|
||||
"confirmdelete": "Are you sure you want to delete this item? It cannot be recovered. Job line statuses will not be updated and may require manual review. ",
|
||||
"email": "Send by Email",
|
||||
"inthisorder": "Parts in this Order",
|
||||
"is_quote": "Parts Quote?",
|
||||
"mark_as_received": "Mark as Received?",
|
||||
"newpartsorder": "New Parts Order",
|
||||
"notyetordered": "This part has not yet been ordered.",
|
||||
@@ -2379,6 +2397,7 @@
|
||||
"job_costing_ro_date_summary": "Job Costing by RO - Summary",
|
||||
"job_costing_ro_estimator": "Job Costing by Estimator",
|
||||
"job_costing_ro_ins_co": "Job Costing by RO Source",
|
||||
"jobs_reconcile": "Parts/Sublet/Labor Reconciliation",
|
||||
"lag_time": "Lag Time",
|
||||
"open_orders": "Open Orders by Date",
|
||||
"open_orders_csr": "Open Orders by CSR",
|
||||
@@ -2445,6 +2464,7 @@
|
||||
"calendarperiod": "Periods based on calendar weeks/months.",
|
||||
"dailyactual": "Actual (D)",
|
||||
"dailytarget": "Daily",
|
||||
"efficiencyoverperiod": "Efficiency over Selected Dates",
|
||||
"jobs": "Jobs",
|
||||
"lastmonth": "Last Month",
|
||||
"lastweek": "Last Week",
|
||||
@@ -2668,6 +2688,7 @@
|
||||
"users": {
|
||||
"errors": {
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "User account disabled. ",
|
||||
"auth/user-not-found": "A user with this email does not exist.",
|
||||
"auth/wrong-password": "The email and password combination you provided is incorrect."
|
||||
}
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"other": "",
|
||||
"reconciled": "",
|
||||
"unreconciled": ""
|
||||
},
|
||||
"validation": {
|
||||
"atleastone": ""
|
||||
}
|
||||
},
|
||||
"bills": {
|
||||
@@ -502,6 +505,7 @@
|
||||
"default_imported": "",
|
||||
"default_invoiced": "",
|
||||
"default_ordered": "",
|
||||
"default_quote": "",
|
||||
"default_received": "",
|
||||
"default_returned": "",
|
||||
"default_scheduled": "",
|
||||
@@ -674,6 +678,7 @@
|
||||
"refuelqty": ""
|
||||
},
|
||||
"correctdataonform": "",
|
||||
"dateinpast": "",
|
||||
"dlexpirebeforereturn": "",
|
||||
"driverinformation": "",
|
||||
"findcontract": "",
|
||||
@@ -748,6 +753,7 @@
|
||||
"status": {
|
||||
"in": "",
|
||||
"inservice": "",
|
||||
"leasereturn": "",
|
||||
"out": "",
|
||||
"sold": ""
|
||||
},
|
||||
@@ -916,7 +922,8 @@
|
||||
},
|
||||
"successes": {
|
||||
"delete": "Empleado eliminado con éxito.",
|
||||
"save": "Empleado guardado con éxito."
|
||||
"save": "Empleado guardado con éxito.",
|
||||
"vacationadded": ""
|
||||
},
|
||||
"validation": {
|
||||
"unique_employee_number": ""
|
||||
@@ -961,7 +968,8 @@
|
||||
},
|
||||
"errors": {
|
||||
"fcm": "",
|
||||
"notfound": ""
|
||||
"notfound": "",
|
||||
"sizelimit": ""
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "",
|
||||
@@ -1109,6 +1117,7 @@
|
||||
},
|
||||
"joblines": {
|
||||
"actions": {
|
||||
"converttolabor": "",
|
||||
"new": ""
|
||||
},
|
||||
"errors": {
|
||||
@@ -1174,7 +1183,9 @@
|
||||
"unq_seq": "Seq #"
|
||||
},
|
||||
"labels": {
|
||||
"adjustmenttobeadded": "",
|
||||
"billref": "",
|
||||
"convertedtolabor": "",
|
||||
"edit": "Línea de edición",
|
||||
"ioucreated": "",
|
||||
"new": "Nueva línea",
|
||||
@@ -1562,6 +1573,7 @@
|
||||
"closeconfirm": "",
|
||||
"closejob": "",
|
||||
"contracts": "",
|
||||
"convertedtolabor": "",
|
||||
"cost": "",
|
||||
"cost_Additional": "",
|
||||
"cost_labor": "",
|
||||
@@ -1604,6 +1616,7 @@
|
||||
"duplicateconfirm": "",
|
||||
"employeeassignments": "",
|
||||
"estimatelines": "",
|
||||
"estimator": "",
|
||||
"existing_jobs": "Empleos existentes",
|
||||
"federal_tax_amt": "",
|
||||
"gpdollars": "",
|
||||
@@ -1907,7 +1920,9 @@
|
||||
"labels": {
|
||||
"addtorelatedro": "",
|
||||
"newnoteplaceholder": "Agrega una nota...",
|
||||
"notetoadd": ""
|
||||
"notetoadd": "",
|
||||
"systemnotes": "",
|
||||
"usernotes": ""
|
||||
},
|
||||
"successes": {
|
||||
"create": "Nota creada con éxito.",
|
||||
@@ -1926,6 +1941,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"noaccess": "El registro no existe o no tiene acceso a él.",
|
||||
"saving": "",
|
||||
"selectexistingornew": ""
|
||||
},
|
||||
"fields": {
|
||||
@@ -1946,7 +1962,8 @@
|
||||
"ownr_st": "Provincia del estado",
|
||||
"ownr_title": "Título",
|
||||
"ownr_zip": "código postal",
|
||||
"preferred_contact": "Método de Contacto Preferido"
|
||||
"preferred_contact": "Método de Contacto Preferido",
|
||||
"tax_number": ""
|
||||
},
|
||||
"forms": {
|
||||
"address": "",
|
||||
@@ -2012,6 +2029,7 @@
|
||||
"confirmdelete": "",
|
||||
"email": "Enviar por correo electrónico",
|
||||
"inthisorder": "Partes en este pedido",
|
||||
"is_quote": "",
|
||||
"mark_as_received": "",
|
||||
"newpartsorder": "",
|
||||
"notyetordered": "",
|
||||
@@ -2379,6 +2397,7 @@
|
||||
"job_costing_ro_date_summary": "",
|
||||
"job_costing_ro_estimator": "",
|
||||
"job_costing_ro_ins_co": "",
|
||||
"jobs_reconcile": "",
|
||||
"lag_time": "",
|
||||
"open_orders": "",
|
||||
"open_orders_csr": "",
|
||||
@@ -2445,6 +2464,7 @@
|
||||
"calendarperiod": "",
|
||||
"dailyactual": "",
|
||||
"dailytarget": "",
|
||||
"efficiencyoverperiod": "",
|
||||
"jobs": "",
|
||||
"lastmonth": "",
|
||||
"lastweek": "",
|
||||
@@ -2668,6 +2688,7 @@
|
||||
"users": {
|
||||
"errors": {
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "",
|
||||
"auth/user-not-found": "",
|
||||
"auth/wrong-password": ""
|
||||
}
|
||||
|
||||
@@ -136,6 +136,9 @@
|
||||
"other": "",
|
||||
"reconciled": "",
|
||||
"unreconciled": ""
|
||||
},
|
||||
"validation": {
|
||||
"atleastone": ""
|
||||
}
|
||||
},
|
||||
"bills": {
|
||||
@@ -502,6 +505,7 @@
|
||||
"default_imported": "",
|
||||
"default_invoiced": "",
|
||||
"default_ordered": "",
|
||||
"default_quote": "",
|
||||
"default_received": "",
|
||||
"default_returned": "",
|
||||
"default_scheduled": "",
|
||||
@@ -674,6 +678,7 @@
|
||||
"refuelqty": ""
|
||||
},
|
||||
"correctdataonform": "",
|
||||
"dateinpast": "",
|
||||
"dlexpirebeforereturn": "",
|
||||
"driverinformation": "",
|
||||
"findcontract": "",
|
||||
@@ -748,6 +753,7 @@
|
||||
"status": {
|
||||
"in": "",
|
||||
"inservice": "",
|
||||
"leasereturn": "",
|
||||
"out": "",
|
||||
"sold": ""
|
||||
},
|
||||
@@ -916,7 +922,8 @@
|
||||
},
|
||||
"successes": {
|
||||
"delete": "L'employé a bien été supprimé.",
|
||||
"save": "L'employé a enregistré avec succès."
|
||||
"save": "L'employé a enregistré avec succès.",
|
||||
"vacationadded": ""
|
||||
},
|
||||
"validation": {
|
||||
"unique_employee_number": ""
|
||||
@@ -961,7 +968,8 @@
|
||||
},
|
||||
"errors": {
|
||||
"fcm": "",
|
||||
"notfound": ""
|
||||
"notfound": "",
|
||||
"sizelimit": ""
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "",
|
||||
@@ -1109,6 +1117,7 @@
|
||||
},
|
||||
"joblines": {
|
||||
"actions": {
|
||||
"converttolabor": "",
|
||||
"new": ""
|
||||
},
|
||||
"errors": {
|
||||
@@ -1174,7 +1183,9 @@
|
||||
"unq_seq": "Seq #"
|
||||
},
|
||||
"labels": {
|
||||
"adjustmenttobeadded": "",
|
||||
"billref": "",
|
||||
"convertedtolabor": "",
|
||||
"edit": "Ligne d'édition",
|
||||
"ioucreated": "",
|
||||
"new": "Nouvelle ligne",
|
||||
@@ -1562,6 +1573,7 @@
|
||||
"closeconfirm": "",
|
||||
"closejob": "",
|
||||
"contracts": "",
|
||||
"convertedtolabor": "",
|
||||
"cost": "",
|
||||
"cost_Additional": "",
|
||||
"cost_labor": "",
|
||||
@@ -1604,6 +1616,7 @@
|
||||
"duplicateconfirm": "",
|
||||
"employeeassignments": "",
|
||||
"estimatelines": "",
|
||||
"estimator": "",
|
||||
"existing_jobs": "Emplois existants",
|
||||
"federal_tax_amt": "",
|
||||
"gpdollars": "",
|
||||
@@ -1907,7 +1920,9 @@
|
||||
"labels": {
|
||||
"addtorelatedro": "",
|
||||
"newnoteplaceholder": "Ajouter une note...",
|
||||
"notetoadd": ""
|
||||
"notetoadd": "",
|
||||
"systemnotes": "",
|
||||
"usernotes": ""
|
||||
},
|
||||
"successes": {
|
||||
"create": "Remarque créée avec succès.",
|
||||
@@ -1926,6 +1941,7 @@
|
||||
},
|
||||
"errors": {
|
||||
"noaccess": "L'enregistrement n'existe pas ou vous n'y avez pas accès.",
|
||||
"saving": "",
|
||||
"selectexistingornew": ""
|
||||
},
|
||||
"fields": {
|
||||
@@ -1946,7 +1962,8 @@
|
||||
"ownr_st": "Etat / Province",
|
||||
"ownr_title": "Titre",
|
||||
"ownr_zip": "Zip / code postal",
|
||||
"preferred_contact": "Méthode de contact préférée"
|
||||
"preferred_contact": "Méthode de contact préférée",
|
||||
"tax_number": ""
|
||||
},
|
||||
"forms": {
|
||||
"address": "",
|
||||
@@ -2012,6 +2029,7 @@
|
||||
"confirmdelete": "",
|
||||
"email": "Envoyé par email",
|
||||
"inthisorder": "Pièces dans cette commande",
|
||||
"is_quote": "",
|
||||
"mark_as_received": "",
|
||||
"newpartsorder": "",
|
||||
"notyetordered": "",
|
||||
@@ -2379,6 +2397,7 @@
|
||||
"job_costing_ro_date_summary": "",
|
||||
"job_costing_ro_estimator": "",
|
||||
"job_costing_ro_ins_co": "",
|
||||
"jobs_reconcile": "",
|
||||
"lag_time": "",
|
||||
"open_orders": "",
|
||||
"open_orders_csr": "",
|
||||
@@ -2445,6 +2464,7 @@
|
||||
"calendarperiod": "",
|
||||
"dailyactual": "",
|
||||
"dailytarget": "",
|
||||
"efficiencyoverperiod": "",
|
||||
"jobs": "",
|
||||
"lastmonth": "",
|
||||
"lastweek": "",
|
||||
@@ -2668,6 +2688,7 @@
|
||||
"users": {
|
||||
"errors": {
|
||||
"signinerror": {
|
||||
"auth/user-disabled": "",
|
||||
"auth/user-not-found": "",
|
||||
"auth/wrong-password": ""
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ const AuditTrailMapping = {
|
||||
i18n.t("audit_trail.messages.jobspartsorder", { order_number }),
|
||||
jobspartsreturn: (order_number) =>
|
||||
i18n.t("audit_trail.messages.jobspartsreturn", { order_number }),
|
||||
jobmodifylbradj: () => i18n.t("audit_trail.messages.jobmodifylbradj", {}),
|
||||
jobmodifylbradj: ({ mod_lbr_ty, hours }) =>
|
||||
i18n.t("audit_trail.messages.jobmodifylbradj", { mod_lbr_ty, hours }),
|
||||
billposted: (invoice_number) =>
|
||||
i18n.t("audit_trail.messages.billposted", { invoice_number }),
|
||||
billupdated: (invoice_number) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { gql } from "@apollo/client";
|
||||
import { notification } from "antd";
|
||||
import axios from "axios";
|
||||
import jsreport from "jsreport-browser-client-dist";
|
||||
import jsreport from "@jsreport/browser-client";
|
||||
import _ from "lodash";
|
||||
import moment from "moment";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
@@ -9,7 +9,9 @@ import { setEmailOptions } from "../redux/email/email.actions";
|
||||
import { store } from "../redux/store";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import { TemplateList } from "./TemplateConstants";
|
||||
|
||||
const server = process.env.REACT_APP_REPORTS_SERVER_URL;
|
||||
|
||||
jsreport.serverUrl = server;
|
||||
|
||||
const Templates = TemplateList();
|
||||
@@ -21,6 +23,10 @@ export default async function RenderTemplate(
|
||||
renderAsExcel = false,
|
||||
renderAsText = false
|
||||
) {
|
||||
if (window.jsr3) {
|
||||
jsreport.serverUrl = "https://reports3.test.imex.online/";
|
||||
}
|
||||
|
||||
//Query assets that match the template name. Must be in format <<templateName>>.query
|
||||
let { contextData, useShopSpecificTemplate } = await fetchContextData(
|
||||
templateObject
|
||||
@@ -69,7 +75,7 @@ export default async function RenderTemplate(
|
||||
};
|
||||
|
||||
try {
|
||||
const render = await jsreport.renderAsync(reportRequest);
|
||||
const render = await jsreport.render(reportRequest);
|
||||
|
||||
if (!renderAsHtml) {
|
||||
render.download(
|
||||
@@ -103,17 +109,18 @@ export default async function RenderTemplate(
|
||||
}),
|
||||
},
|
||||
};
|
||||
console.log("PDFREQ", pdfRequest);
|
||||
const pdfRender = await jsreport.renderAsync(pdfRequest);
|
||||
pdf = pdfRender.toDataURI();
|
||||
|
||||
const pdfRender = await jsreport.render(pdfRequest);
|
||||
pdf = await pdfRender.toDataURI();
|
||||
}
|
||||
const html = await render.toString();
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve({
|
||||
pdf,
|
||||
filename:
|
||||
Templates[templateObject.name] &&
|
||||
Templates[templateObject.name].title,
|
||||
html: render.toString(),
|
||||
html,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -152,6 +159,9 @@ export async function RenderTemplates(
|
||||
// (template) => template.name === item.templateObject.name
|
||||
// );
|
||||
// });
|
||||
if (window.jsr3) {
|
||||
jsreport.serverUrl = "https://reports3.test.imex.online/";
|
||||
}
|
||||
|
||||
unsortedTemplatesAndData.sort(function (a, b) {
|
||||
return (
|
||||
@@ -242,13 +252,11 @@ export async function RenderTemplates(
|
||||
};
|
||||
|
||||
try {
|
||||
const render = await jsreport.renderAsync(reportRequest);
|
||||
const render = await jsreport.render(reportRequest);
|
||||
if (!renderAsHtml) {
|
||||
render.download("Speed Print");
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(render.toString());
|
||||
});
|
||||
return render.toString();
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({ message: JSON.stringify(error) });
|
||||
|
||||
@@ -1534,6 +1534,18 @@ export const TemplateList = (type, context) => {
|
||||
},
|
||||
group: "purchases",
|
||||
},
|
||||
jobs_reconcile: {
|
||||
title: i18n.t("reportcenter.templates.jobs_reconcile"),
|
||||
subject: i18n.t("reportcenter.templates.jobs_reconcile"),
|
||||
key: "jobs_reconcile",
|
||||
//idtype: "vendor",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||
field: i18n.t("jobs.fields.date_invoiced"),
|
||||
},
|
||||
group: "jobs",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(!type || type === "courtesycarcontract"
|
||||
|
||||
@@ -23,9 +23,9 @@ export default async function FcmHandler({ client, payload }) {
|
||||
updated_at(oldupdated0) {
|
||||
return new Date();
|
||||
},
|
||||
messages_aggregate(cached) {
|
||||
return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||
},
|
||||
// messages_aggregate(cached) {
|
||||
// return { aggregate: { count: cached.aggregate.count + 1 } };
|
||||
// },
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
40
client/src/utils/useLocalStorage.js
Normal file
40
client/src/utils/useLocalStorage.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState } from "react";
|
||||
|
||||
export default function useLocalStorage(key, initialValue) {
|
||||
// State to store our value
|
||||
// Pass initial state function to useState so logic is only executed once
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return initialValue;
|
||||
}
|
||||
try {
|
||||
// Get from local storage by key
|
||||
const item = window.localStorage.getItem(key);
|
||||
// Parse stored json or if none return initialValue
|
||||
return item ? JSON.parse(item) : initialValue;
|
||||
} catch (error) {
|
||||
// If error also return initialValue
|
||||
console.log(error);
|
||||
return initialValue;
|
||||
}
|
||||
});
|
||||
// Return a wrapped version of useState's setter function that ...
|
||||
// ... persists the new value to localStorage.
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
// Allow value to be a function so we have same API as useState
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value;
|
||||
// Save state
|
||||
setStoredValue(valueToStore);
|
||||
// Save to local storage
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
}
|
||||
} catch (error) {
|
||||
// A more advanced implementation would handle the error case
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
@@ -1918,6 +1918,11 @@
|
||||
"@types/yargs" "^16.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jsreport/browser-client@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jsreport/browser-client/-/browser-client-3.1.0.tgz#a84011087ca8a29a6dc6a852fa05ffaf1983a679"
|
||||
integrity sha512-ZElwn2KRIzkUzAyD5UKGxULZUhokWuPOlMzrmiur4WirqH3yoiHlOJEdnRGkjjE/fhZzCR8gBFZ/TuOW/fsOIw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
Must set the environment variables using:
|
||||
|
||||
firebase functions:config:set auth.graphql_endpoint="https://bodyshop-dev-db.herokuapp.com/v1/graphql" auth.hasura_secret_admin_key="Dev-BodyShopApp!"
|
||||
firebase functions:config:set auth.graphql_endpoint="https://db.development.bodyshop.app/v1/graphql" auth.hasura_secret_admin_key="Dev-BodyShopApp!"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
version: 2
|
||||
endpoint: https://bodyshop-dev-db.herokuapp.com
|
||||
endpoint: https://db.development.bodyshop.app
|
||||
admin_secret: Dev-BodyShopApp!
|
||||
metadata_directory: metadata
|
||||
actions:
|
||||
|
||||
@@ -2384,6 +2384,8 @@
|
||||
- bett_tax
|
||||
- bett_type
|
||||
- cert_part
|
||||
- convertedtolbr
|
||||
- convertedtolbr_data
|
||||
- created_at
|
||||
- db_hrs
|
||||
- db_price
|
||||
@@ -2447,6 +2449,8 @@
|
||||
- bett_tax
|
||||
- bett_type
|
||||
- cert_part
|
||||
- convertedtolbr
|
||||
- convertedtolbr_data
|
||||
- created_at
|
||||
- db_hrs
|
||||
- db_price
|
||||
@@ -2521,6 +2525,8 @@
|
||||
- bett_tax
|
||||
- bett_type
|
||||
- cert_part
|
||||
- convertedtolbr
|
||||
- convertedtolbr_data
|
||||
- created_at
|
||||
- db_hrs
|
||||
- db_price
|
||||
@@ -4009,6 +4015,7 @@
|
||||
- ownr_zip
|
||||
- preferred_contact
|
||||
- shopid
|
||||
- tax_number
|
||||
- updated_at
|
||||
filter:
|
||||
bodyshop:
|
||||
@@ -4044,6 +4051,7 @@
|
||||
- ownr_zip
|
||||
- preferred_contact
|
||||
- shopid
|
||||
- tax_number
|
||||
- updated_at
|
||||
filter:
|
||||
bodyshop:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."joblines" add column "convertedtolbr" boolean
|
||||
-- not null default 'false';
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."joblines" add column "convertedtolbr" boolean
|
||||
not null default 'false';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."joblines" add column "convertedtolbr_data" jsonb
|
||||
-- not null default jsonb_build_object();
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."joblines" add column "convertedtolbr_data" jsonb
|
||||
not null default jsonb_build_object();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."owners" add column "tax_number" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."owners" add column "tax_number" text
|
||||
null;
|
||||
12049
package-lock.json
generated
Normal file
12049
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
server.js
16
server.js
@@ -157,7 +157,21 @@ app.post(
|
||||
fb.unsubscribe
|
||||
);
|
||||
app.post("/adm/updateuser", fb.validateFirebaseIdToken, fb.updateUser);
|
||||
app.post("/adm/getuser", fb.validateFirebaseIdToken, fb.getUser);
|
||||
app.post("/adm/createuser", fb.validateFirebaseIdToken, fb.createUser);
|
||||
const adm = require("./server/admin/adminops");
|
||||
app.post(
|
||||
"/adm/createassociation",
|
||||
fb.validateFirebaseIdToken,
|
||||
fb.validateAdmin,
|
||||
adm.createAssociation
|
||||
);
|
||||
app.post(
|
||||
"/adm/createshop",
|
||||
fb.validateFirebaseIdToken,
|
||||
fb.validateAdmin,
|
||||
adm.createShop
|
||||
);
|
||||
|
||||
//Stripe Processing
|
||||
var stripe = require("./server/stripe/payment");
|
||||
@@ -216,7 +230,7 @@ server.listen(port, (error) => {
|
||||
if (error) throw error;
|
||||
logger.log(
|
||||
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server running on port ${port}`,
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"api"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -179,7 +179,6 @@ exports.default = async (req, res) => {
|
||||
ret.push({ paymentid: payment.id, success: true });
|
||||
} catch (error) {
|
||||
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
|
||||
|
||||
error:
|
||||
(error && error.authResponse && error.authResponse.body) ||
|
||||
(error && error.message),
|
||||
@@ -217,7 +216,10 @@ exports.default = async (req, res) => {
|
||||
res.status(200).json(ret);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.log("qbo-payment-create-error", "ERROR", req.user.email, { error: error.message, stack: error.stack });
|
||||
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
res.status(400).json(error);
|
||||
}
|
||||
};
|
||||
@@ -240,7 +242,7 @@ async function InsertPayment(
|
||||
|
||||
if (invoices && invoices.length !== 1) {
|
||||
throw new Error(
|
||||
`More than 1 invoice with DocNumber ${payment.ro_number} found.`
|
||||
`More than 1 invoice with DocNumber ${payment.job.ro_number} found.`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
68
server/admin/adminops.js
Normal file
68
server/admin/adminops.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const path = require("path");
|
||||
|
||||
const _ = require("lodash");
|
||||
const logger = require("../utils/logger");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
`.env.${process.env.NODE_ENV || "development"}`
|
||||
),
|
||||
});
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
|
||||
exports.createAssociation = async (req, res) => {
|
||||
logger.log("admin-create-association", "ADMIN", req.user.email, null, {
|
||||
request: req.body,
|
||||
ioadmin: true,
|
||||
});
|
||||
const { shopid, authlevel, useremail } = req.body;
|
||||
|
||||
const result = await client.request(
|
||||
`mutation INSERT_ASSOCIATION($assoc: associations_insert_input!){
|
||||
insert_associations_one(object:$assoc){
|
||||
id
|
||||
authlevel
|
||||
useremail
|
||||
active
|
||||
}
|
||||
}`,
|
||||
{
|
||||
assoc: { shopid, authlevel, useremail, active: false },
|
||||
}
|
||||
);
|
||||
res.json(result);
|
||||
};
|
||||
exports.createShop = async (req, res) => {
|
||||
logger.log("admin-create-shop", "ADMIN", req.user.email, null, {
|
||||
request: req.body,
|
||||
ioadmin: true,
|
||||
});
|
||||
const { bodyshop, ronum } = req.body;
|
||||
|
||||
try {
|
||||
const result = await client.request(
|
||||
`mutation INSERT_BODYSHOPS($bs: bodyshops_insert_input!){
|
||||
insert_bodyshops_one(object:$bs){
|
||||
id
|
||||
|
||||
}
|
||||
}`,
|
||||
{
|
||||
bs: {
|
||||
...bodyshop,
|
||||
counters: {
|
||||
data: [
|
||||
{ countertype: "ronum", count: ronum },
|
||||
{ countertype: "ihbnum", count: 1 },
|
||||
{ countertype: "paymentnum", count: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
res.status(500).json(error);
|
||||
}
|
||||
};
|
||||
@@ -39,7 +39,7 @@ exports.default = async (req, res) => {
|
||||
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);
|
||||
|
||||
const specificShopIds = req.body.bodyshopIds; // ['uuid]
|
||||
const { start, end } = req.body; //YYYY-MM-DD
|
||||
const { start, end, skipUpload } = req.body; //YYYY-MM-DD
|
||||
const allxmlsToUpload = [];
|
||||
const allErrors = [];
|
||||
try {
|
||||
@@ -107,12 +107,13 @@ exports.default = async (req, res) => {
|
||||
} catch (error) {
|
||||
//Error at the shop level.
|
||||
logger.log("autohouse-error-shop", "ERROR", "api", bodyshop.id, {
|
||||
error,
|
||||
...error,
|
||||
});
|
||||
|
||||
allErrors.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
autuhouseid: bodyshop.autuhouseid,
|
||||
fatal: true,
|
||||
errors: [error.toString()],
|
||||
});
|
||||
@@ -120,22 +121,39 @@ exports.default = async (req, res) => {
|
||||
allErrors.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
errors: erroredJobs,
|
||||
autohouseid: bodyshop.autohouseid,
|
||||
errors: erroredJobs.map((ej) => ({
|
||||
ro_number: ej.job?.ro_number,
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// for (const xmlObj of allxmlsToUpload) {
|
||||
// fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||
// }
|
||||
if (skipUpload) {
|
||||
for (const xmlObj of allxmlsToUpload) {
|
||||
fs.writeFileSync(`./logs/${xmlObj.filename}`, xmlObj.xml);
|
||||
}
|
||||
|
||||
// res.json(allxmlsToUpload);
|
||||
// return;
|
||||
res.json(allxmlsToUpload);
|
||||
sendServerEmail({
|
||||
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
|
||||
Uploaded: ${JSON.stringify(
|
||||
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
|
||||
errors,
|
||||
...errors,
|
||||
})
|
||||
);
|
||||
try {
|
||||
@@ -160,7 +178,7 @@ exports.default = async (req, res) => {
|
||||
//***TODO Change filing naming when creating the cron job. IM_ShopInternalName_DDMMYYYY_HHMMSS.xml
|
||||
} catch (error) {
|
||||
logger.log("autohouse-sftp-error", "ERROR", "api", null, {
|
||||
error,
|
||||
...error,
|
||||
});
|
||||
} finally {
|
||||
sftp.end();
|
||||
@@ -169,7 +187,7 @@ exports.default = async (req, res) => {
|
||||
subject: `Autohouse Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
|
||||
Uploaded: ${JSON.stringify(
|
||||
allxmlsToUpload.map((x) => x.filename),
|
||||
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
|
||||
null,
|
||||
2
|
||||
)}
|
||||
@@ -185,7 +203,12 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
//Level 2
|
||||
|
||||
if (!job.job_totals) {
|
||||
errorCallback({ job, error: { toString: () => "No job totals for RO." } });
|
||||
errorCallback({
|
||||
jobid: job.id,
|
||||
job: job,
|
||||
ro_number: job.ro_number,
|
||||
error: { toString: () => "No job totals for RO." },
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -658,13 +681,14 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
error,
|
||||
});
|
||||
|
||||
errorCallback({ job, error });
|
||||
errorCallback({ jobid: job.id, ro_number: job.ro_number, error });
|
||||
}
|
||||
};
|
||||
|
||||
const CreateCosts = (job) => {
|
||||
//Create a mapping based on AH Requirements
|
||||
|
||||
//For DMS, the keys in the object below are the CIECA part types.
|
||||
const billTotalsByCostCenters = job.bills.reduce((bill_acc, bill_val) => {
|
||||
//At the bill level.
|
||||
bill_val.billlines.map((line_val) => {
|
||||
@@ -731,7 +755,7 @@ const CreateCosts = (job) => {
|
||||
}).multiply(job.job_totals.rates.mash.hours)
|
||||
);
|
||||
}
|
||||
|
||||
//Uses CIECA Labor types.
|
||||
const ticketTotalsByCostCenter = job.timetickets.reduce(
|
||||
(ticket_acc, ticket_val) => {
|
||||
//At the invoice level.
|
||||
@@ -750,7 +774,43 @@ const CreateCosts = (job) => {
|
||||
},
|
||||
{}
|
||||
);
|
||||
const defaultCosts = job.bodyshop.md_responsibility_centers.defaults.costs;
|
||||
//CIECA STANDARD MAPPING OBJECT.
|
||||
|
||||
const ciecaObj = {
|
||||
ATS: "ATS",
|
||||
LA1: "LA1",
|
||||
LA2: "LA2",
|
||||
LA3: "LA3",
|
||||
LA4: "LA4",
|
||||
LAA: "LAA",
|
||||
LAB: "LAB",
|
||||
LAD: "LAD",
|
||||
LAE: "LAE",
|
||||
LAF: "LAF",
|
||||
LAG: "LAG",
|
||||
LAM: "LAM",
|
||||
LAR: "LAR",
|
||||
LAS: "LAS",
|
||||
LAU: "LAU",
|
||||
PAA: "PAA",
|
||||
PAC: "PAC",
|
||||
PAG: "PAG",
|
||||
PAL: "PAL",
|
||||
PAM: "PAM",
|
||||
PAN: "PAN",
|
||||
PAO: "PAO",
|
||||
PAP: "PAP",
|
||||
PAR: "PAR",
|
||||
PAS: "PAS",
|
||||
TOW: "TOW",
|
||||
MAPA: "MAPA",
|
||||
MASH: "MASH",
|
||||
PASL: "PASL",
|
||||
};
|
||||
const defaultCosts =
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
|
||||
? ciecaObj
|
||||
: job.bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
return {
|
||||
PartsTotalCost: Object.keys(billTotalsByCostCenters).reduce((acc, key) => {
|
||||
@@ -852,7 +912,9 @@ const GenerateDetailLines = (job, line, statuses) => {
|
||||
OriginalCost: null,
|
||||
OriginalInvoiceNumber: null,
|
||||
PriceEach: line.act_price || 0,
|
||||
PartNumber: _.escape(line.oem_partno),
|
||||
PartNumber: line.oem_partno
|
||||
? line.oem_partno.replace(/[^\x00-\x7F]/g, "")
|
||||
: "",
|
||||
ProfitPercent: null,
|
||||
PurchaseOrderNumber: null,
|
||||
Qty: line.part_qty || 0,
|
||||
|
||||
@@ -10,7 +10,8 @@ let nodemailer = require("nodemailer");
|
||||
let aws = require("aws-sdk");
|
||||
const logger = require("../utils/logger");
|
||||
const ses = new aws.SES({
|
||||
apiVersion: "2010-12-01",
|
||||
apiVersion: "latest",
|
||||
|
||||
region: "ca-central-1",
|
||||
});
|
||||
|
||||
@@ -43,6 +44,7 @@ exports.sendServerEmail = async function ({ subject, text }) {
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.log("server-email-failure", "error", null, null, error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
};
|
||||
exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
|
||||
@@ -153,7 +155,7 @@ exports.sendEmail = async (req, res) => {
|
||||
error: err,
|
||||
});
|
||||
|
||||
res.json({ success: false, error: err });
|
||||
res.status(500).json({ success: false, error: err });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
var admin = require("firebase-admin");
|
||||
const logger = require("../utils/logger");
|
||||
const path = require("path");
|
||||
const { auth } = require("firebase-admin");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(
|
||||
process.cwd(),
|
||||
`.env.${process.env.NODE_ENV || "development"}`
|
||||
),
|
||||
});
|
||||
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
var serviceAccount = require(process.env.FIREBASE_ADMINSDK_JSON);
|
||||
|
||||
admin.initializeApp({
|
||||
@@ -19,54 +20,61 @@ exports.admin = admin;
|
||||
|
||||
const adminEmail = [
|
||||
"patrick@imex.dev",
|
||||
"patrick@imex.text",
|
||||
//"patrick@imex.test",
|
||||
"patrick@imex.prod",
|
||||
"patrick@imexsystems.ca",
|
||||
"patrick@thinkimex.com",
|
||||
];
|
||||
|
||||
exports.createUser = (req, res) => {
|
||||
logger.log("admin-create-user", "WARN", req.user.email, null, {
|
||||
exports.createUser = async (req, res) => {
|
||||
logger.log("admin-create-user", "ADMIN", req.user.email, null, {
|
||||
request: req.body,
|
||||
ioadmin: true,
|
||||
});
|
||||
if (!adminEmail.includes(req.user.email)) {
|
||||
logger.log(
|
||||
"admin-create-user-unauthorized",
|
||||
"ERROR",
|
||||
req.user.email,
|
||||
null,
|
||||
|
||||
const { email, displayName, password, shopid, authlevel } = req.body;
|
||||
try {
|
||||
const userRecord = await admin
|
||||
.auth()
|
||||
.createUser({ email, displayName, password });
|
||||
|
||||
// See the UserRecord reference doc for the contents of userRecord.
|
||||
|
||||
const result = await client.request(
|
||||
`
|
||||
mutation INSERT_USER($user: users_insert_input!) {
|
||||
insert_users_one(object: $user) {
|
||||
email
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
request: req.body,
|
||||
user: req.user,
|
||||
user: {
|
||||
email,
|
||||
authid: userRecord.uid,
|
||||
associations: {
|
||||
data: [{ shopid, authlevel, active: true }],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
res.sendStatus(404);
|
||||
}
|
||||
const { email, displayName, password } = req.body;
|
||||
admin
|
||||
.auth()
|
||||
.createUser({ email, displayName, password })
|
||||
.then((userRecord) => {
|
||||
// See the UserRecord reference doc for the contents of userRecord.
|
||||
|
||||
logger.log("admin-update-user-success", "DEBUG", req.user.email, null, {
|
||||
userRecord,
|
||||
});
|
||||
res.json(userRecord);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
|
||||
error,
|
||||
});
|
||||
res.status(500).json(error);
|
||||
res.json({ userRecord, result });
|
||||
} catch (error) {
|
||||
logger.log("admin-update-user-error", "ERROR", req.user.email, null, {
|
||||
error,
|
||||
});
|
||||
res.status(500).json(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.updateUser = (req, res) => {
|
||||
logger.log("admin-update-user", "WARN", req.user.email, null, {
|
||||
logger.log("admin-update-user", "ADMIN", req.user.email, null, {
|
||||
request: req.body,
|
||||
ioadmin: true,
|
||||
});
|
||||
if (!adminEmail.includes(req.user.email)) {
|
||||
|
||||
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
|
||||
logger.log(
|
||||
"admin-update-user-unauthorized",
|
||||
"ERROR",
|
||||
@@ -78,6 +86,7 @@ exports.updateUser = (req, res) => {
|
||||
}
|
||||
);
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
admin
|
||||
@@ -98,8 +107,9 @@ exports.updateUser = (req, res) => {
|
||||
.then((userRecord) => {
|
||||
// See the UserRecord reference doc for the contents of userRecord.
|
||||
|
||||
logger.log("admin-update-user-success", "DEBUG", req.user.email, null, {
|
||||
logger.log("admin-update-user-success", "ADMIN", req.user.email, null, {
|
||||
userRecord,
|
||||
ioadmin: true,
|
||||
});
|
||||
res.json(userRecord);
|
||||
})
|
||||
@@ -111,6 +121,41 @@ exports.updateUser = (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
exports.getUser = (req, res) => {
|
||||
logger.log("admin-get-user", "ADMIN", req.user.email, null, {
|
||||
request: req.body,
|
||||
ioadmin: true,
|
||||
});
|
||||
|
||||
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
|
||||
logger.log(
|
||||
"admin-update-user-unauthorized",
|
||||
"ERROR",
|
||||
req.user.email,
|
||||
null,
|
||||
{
|
||||
request: req.body,
|
||||
user: req.user,
|
||||
}
|
||||
);
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
}
|
||||
|
||||
admin
|
||||
.auth()
|
||||
.getUser(req.body.uid)
|
||||
.then((userRecord) => {
|
||||
res.json(userRecord);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.log("admin-get-user-error", "ERROR", req.user.email, null, {
|
||||
error,
|
||||
});
|
||||
res.status(500).json(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.sendNotification = async (req, res) => {
|
||||
setTimeout(() => {
|
||||
// Send a message to the device corresponding to the provided
|
||||
@@ -212,12 +257,46 @@ exports.validateFirebaseIdToken = async (req, res, next) => {
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.log("api-unauthorized-call", "WARN", null, null, {
|
||||
req,
|
||||
path: req.path,
|
||||
body: req.body,
|
||||
|
||||
type: "unauthroized",
|
||||
error,
|
||||
...error,
|
||||
});
|
||||
|
||||
res.status(403).send("Unauthorized");
|
||||
res.status(401).send("Unauthorized");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
exports.validateAdmin = async (req, res, next) => {
|
||||
if (!adminEmail.includes(req.user.email) && !req.user.ioadmin) {
|
||||
logger.log("admin-validation-failed", "ERROR", req.user.email, null, {
|
||||
request: req.body,
|
||||
user: req.user,
|
||||
});
|
||||
res.sendStatus(404);
|
||||
return;
|
||||
} else {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//Admin claims code.
|
||||
// const uid = "JEqqYlsadwPEXIiyRBR55fflfko1";
|
||||
|
||||
// admin
|
||||
// .auth()
|
||||
// .getUser(uid)
|
||||
// .then((user) => {
|
||||
// console.log(user);
|
||||
// admin.auth().setCustomUserClaims(uid, {
|
||||
// ioadmin: true,
|
||||
// "https://hasura.io/jwt/claims": {
|
||||
// "x-hasura-default-role": "admin",
|
||||
// "x-hasura-allowed-roles": ["admin"],
|
||||
// "x-hasura-user-id": uid,
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -611,6 +611,8 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
|
||||
autohouseid
|
||||
md_responsibility_centers
|
||||
jc_hourly_rates
|
||||
cdk_dealerid
|
||||
pbs_serialnumber
|
||||
timezone
|
||||
}
|
||||
jobs(where: {_and: [{converted: {_eq: true}}, {updated_at: {_gt: $start}}, {updated_at: {_lte: $end}}, {shopid: {_eq: $bodyshopid}}]}) {
|
||||
|
||||
@@ -218,7 +218,7 @@ async function JobCostingMulti(req, res) {
|
||||
(multiSummary.summaryData.totalLaborGp.getAmount() /
|
||||
multiSummary.summaryData.totalLaborSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
multiSummary.summaryData.totalLaborGppercentFormatted = formatGpPercent(
|
||||
multiSummary.summaryData.totalLaborGppercent
|
||||
);
|
||||
@@ -227,7 +227,7 @@ async function JobCostingMulti(req, res) {
|
||||
(multiSummary.summaryData.totalPartsGp.getAmount() /
|
||||
multiSummary.summaryData.totalPartsSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
|
||||
multiSummary.summaryData.totalPartsGppercentFormatted = formatGpPercent(
|
||||
multiSummary.summaryData.totalPartsGppercent
|
||||
@@ -237,7 +237,7 @@ async function JobCostingMulti(req, res) {
|
||||
(multiSummary.summaryData.totalAdditionalGp.getAmount() /
|
||||
multiSummary.summaryData.totalAdditionalSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
|
||||
multiSummary.summaryData.totalAdditionalGppercentFormatted =
|
||||
formatGpPercent(multiSummary.summaryData.totalAdditionalGppercent);
|
||||
@@ -246,7 +246,7 @@ async function JobCostingMulti(req, res) {
|
||||
(multiSummary.summaryData.totalSubletGp.getAmount() /
|
||||
multiSummary.summaryData.totalSubletSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
|
||||
multiSummary.summaryData.totalSubletGppercentFormatted = formatGpPercent(
|
||||
multiSummary.summaryData.totalSubletGppercent
|
||||
@@ -256,7 +256,7 @@ async function JobCostingMulti(req, res) {
|
||||
(multiSummary.summaryData.gpdollars.getAmount() /
|
||||
multiSummary.summaryData.totalSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
|
||||
multiSummary.summaryData.gppercentFormatted = formatGpPercent(
|
||||
multiSummary.summaryData.gppercent
|
||||
@@ -282,7 +282,7 @@ async function JobCostingMulti(req, res) {
|
||||
(
|
||||
(c.gpdollars_dinero.getAmount() / c.sales_dinero.getAmount()) *
|
||||
100
|
||||
).toFixed(2)
|
||||
).toFixed(1)
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -730,9 +730,9 @@ function GenerateCostingData(job) {
|
||||
.add(sale_sublet);
|
||||
const gpdollars = totalSales.subtract(costs);
|
||||
const gppercent = (
|
||||
(gpdollars.getAmount() / totalSales.getAmount()) *
|
||||
(gpdollars.getAmount() / Math.abs(totalSales.getAmount())) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
|
||||
//Push summary data to avoid extra loop.
|
||||
summaryData.totalLaborSales = summaryData.totalLaborSales.add(sale_labor);
|
||||
@@ -823,7 +823,7 @@ function GenerateCostingData(job) {
|
||||
(summaryData.totalLaborGp.getAmount() /
|
||||
summaryData.totalLaborSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
summaryData.totalLaborGppercentFormatted = formatGpPercent(
|
||||
summaryData.totalLaborGppercent
|
||||
);
|
||||
@@ -835,7 +835,7 @@ function GenerateCostingData(job) {
|
||||
(summaryData.totalPartsGp.getAmount() /
|
||||
summaryData.totalPartsSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
summaryData.totalPartsGppercentFormatted = formatGpPercent(
|
||||
summaryData.totalPartsGppercent
|
||||
);
|
||||
@@ -846,7 +846,7 @@ function GenerateCostingData(job) {
|
||||
(summaryData.totalAdditionalGp.getAmount() /
|
||||
summaryData.totalAdditionalSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
summaryData.totalAdditionalGppercentFormatted = formatGpPercent(
|
||||
summaryData.totalAdditionalGppercent
|
||||
);
|
||||
@@ -857,7 +857,7 @@ function GenerateCostingData(job) {
|
||||
(summaryData.totalSubletGp.getAmount() /
|
||||
summaryData.totalSubletSales.getAmount()) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
summaryData.totalSubletGppercentFormatted = formatGpPercent(
|
||||
summaryData.totalSubletGppercent
|
||||
);
|
||||
@@ -866,9 +866,10 @@ function GenerateCostingData(job) {
|
||||
summaryData.totalCost
|
||||
);
|
||||
summaryData.gppercent = (
|
||||
(summaryData.gpdollars.getAmount() / summaryData.totalSales.getAmount()) *
|
||||
(summaryData.gpdollars.getAmount() /
|
||||
Math.abs(summaryData.totalSales.getAmount())) *
|
||||
100
|
||||
).toFixed(2);
|
||||
).toFixed(1);
|
||||
|
||||
if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0;
|
||||
else if (!isFinite(summaryData.gppercent))
|
||||
|
||||
@@ -32,6 +32,8 @@ exports.mixdataUpload = async (req, res) => {
|
||||
explicitArray: false,
|
||||
});
|
||||
|
||||
logger.log("job-mixdata-parse", "DEBUG", req.user.email, inboundRequest);
|
||||
|
||||
const ScaleType = DetermineScaleType(inboundRequest);
|
||||
const RoNumbersFromInboundRequest = GetListOfRos(
|
||||
inboundRequest,
|
||||
@@ -76,6 +78,7 @@ exports.mixdataUpload = async (req, res) => {
|
||||
res.status(500).JSON(error);
|
||||
logger.log("job-mixdata-upload-error", "ERROR", null, null, {
|
||||
error: error.message,
|
||||
...error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ const logger = new graylog2.graylog({
|
||||
});
|
||||
|
||||
function log(message, type, user, record, object) {
|
||||
if (type !== "ioevent")
|
||||
if (type !== "ioevent" && type !== "DEBUG")
|
||||
console.log(message, {
|
||||
type,
|
||||
env: process.env.NODE_ENV || "development",
|
||||
@@ -13,7 +13,7 @@ function log(message, type, user, record, object) {
|
||||
record,
|
||||
...object,
|
||||
});
|
||||
logger.log(message, {
|
||||
logger.log(message, message, {
|
||||
type,
|
||||
env: process.env.NODE_ENV || "development",
|
||||
user,
|
||||
|
||||
23
setadmin.js
Normal file
23
setadmin.js
Normal file
@@ -0,0 +1,23 @@
|
||||
var { admin } = require("./server/firebase/firebase-handler");
|
||||
|
||||
const uidToMakeAdmin = "yTvpfkcNnGckLd1JnoXC7bTdvtu1";
|
||||
|
||||
admin
|
||||
.auth()
|
||||
.getUser(uidToMakeAdmin)
|
||||
.then((user) => {
|
||||
admin
|
||||
.auth()
|
||||
.setCustomUserClaims(uidToMakeAdmin, {
|
||||
...user.customClaims,
|
||||
"https://hasura.io/jwt/claims": {
|
||||
"x-hasura-default-role": "admin",
|
||||
"x-hasura-allowed-roles": ["admin"],
|
||||
"x-hasura-user-id": uidToMakeAdmin,
|
||||
},
|
||||
ioadmin: true,
|
||||
})
|
||||
.then(() => console.log("Success."))
|
||||
.catch((error) => console.log("Error updating claims.", error));
|
||||
})
|
||||
.catch((error) => console.log("Error fetching user.", error));
|
||||
Reference in New Issue
Block a user