Compare commits

...

44 Commits

Author SHA1 Message Date
Patrick Fic
e92827aeb2 Resolve note saving issue. 2022-05-17 11:23:50 -07:00
Patrick Fic
82c13eae9e update NPM manager. 2022-05-13 17:14:03 -07:00
Patrick Fic
cdb8e48f0d Updated CI config. 2022-05-13 16:39:47 -07:00
Patrick Fic
90600cdff4 Local media server bugfixes. 2022-05-13 09:34:19 -07:00
Patrick Fic
cdcfea988f Merged in hotfix/2022-05-10 (pull request #477)
Hotfix/2022 05 10
2022-05-12 23:58:38 +00:00
Patrick Fic
26f58961a0 IO-1875 Add CNR by Vendor 2022-05-12 16:58:02 -07:00
Patrick Fic
8daa0ac154 Update URL to create explorer link. 2022-05-12 16:04:10 -07:00
Patrick Fic
76fee429ea Merge branch 'hotfix/2022-05-10' into release/2022-05-13 2022-05-12 12:40:26 -07:00
Patrick Fic
d1a65530a3 IO-1881 Related RO notes. 2022-05-12 12:35:38 -07:00
Patrick Fic
4613a93d09 IO-1877 Multi line notes presets. 2022-05-12 11:36:11 -07:00
Patrick Fic
faf1d638fb IO-1874 Custom fields for receivables. 2022-05-12 11:34:15 -07:00
Patrick Fic
55144bd621 Added IMS token changes. 2022-05-11 16:31:09 -07:00
Patrick Fic
bbf908e5e1 Merge branch 'hotfix/2022-05-10' into release/2022-05-13 2022-05-10 14:29:22 -07:00
Patrick Fic
18fa00785c Update QBO export query for payments. 2022-05-10 14:25:28 -07:00
Patrick Fic
3192e918a4 Merged in feature/local-images (pull request #464)
feature/local-images

Approved-by: Patrick Fic
2022-05-10 20:18:39 +00:00
Patrick Fic
70e2fd000c Add local media setup to shop config. 2022-05-10 13:05:06 -07:00
Patrick Fic
f9fdd95491 Remove documents components that do not support local media. 2022-05-09 09:56:46 -07:00
Patrick Fic
45354417d0 Merge master into feature/local-images 2022-05-09 08:26:43 -07:00
Patrick Fic
70749cdef5 Merged in release/2022-05-06 (pull request #463)
Release/2022 05 06
2022-05-06 23:46:20 +00:00
Patrick Fic
6c3d29ba91 IO-1853 CSV Generation. 2022-05-06 15:50:05 -07:00
Patrick Fic
eca5e8241a QBO Bill parent by default. 2022-05-06 15:35:34 -07:00
Patrick Fic
cf23266831 IO-1853 Change attendance recipe type to text. 2022-05-06 14:30:26 -07:00
Patrick Fic
09d54722f0 IO-1863 Delete parts return and order line. 2022-05-06 14:25:02 -07:00
Patrick Fic
d78955a8fd IO-1853 Add Attendance Table excel creation. 2022-05-06 10:19:49 -07:00
Patrick Fic
467841bea2 IO-1865 Add employee external ID 2022-05-06 09:46:26 -07:00
Patrick Fic
e56424c9b3 IO-1858 Email Groupings 2022-05-06 09:30:29 -07:00
Patrick Fic
a1e4f3827d Uploads and viewing from bills. 2022-05-05 15:46:58 -07:00
Patrick Fic
5461aae6f6 Base changes to job upload screen. 2022-05-04 18:13:58 -07:00
Patrick Fic
55fa2a9b8d Merged in release/2022-05-06 (pull request #459)
Release/2022 05 06
2022-05-03 18:47:30 +00:00
Patrick Fic
e348110bdd IO-1857 Resolve time ticket update fix. 2022-05-03 11:33:31 -07:00
Patrick Fic
d533423fb6 Add backwards compatibility for log generation. 2022-05-03 11:08:08 -07:00
Patrick Fic
9b4e83705b Add department to QBO Payables. 2022-05-03 09:51:54 -07:00
Patrick Fic
4f6d1d27d5 IO-1855 Change QBO changes to server side. 2022-05-02 15:52:05 -07:00
Patrick Fic
2d9de4703b Merged in release/2022-04-29 (pull request #456)
Test
2022-05-02 01:00:23 +00:00
Patrick Fic
865f4776d0 IO-233 Mixdata schema updates and API. 2022-04-28 09:54:15 -07:00
Patrick Fic
ad6d1202f2 IO-227 Begin PPG 2 way communication. 2022-04-27 16:03:53 -07:00
Patrick Fic
3db613da7f IO-1847 Allow $0 labor line with type selected. 2022-04-26 16:01:50 -07:00
Patrick Fic
c48b0d7b99 IO-1847 Add blank line if no sales lines for QBD export. 2022-04-26 15:56:03 -07:00
Patrick Fic
15cdcdfbea IO-1846 add prior succesful export indicator. 2022-04-26 15:25:51 -07:00
Patrick Fic
39b7280595 Allow clear of deductible status. 2022-04-26 13:38:16 -07:00
Patrick Fic
273542f93b IO-1532 Add job transition tracking server method. 2022-04-26 13:38:07 -07:00
Patrick Fic
6d01199185 IO-1831 Skip PBS vehicle search with no vin 2022-04-25 14:54:36 -07:00
Patrick Fic
db0ade9791 IO-1680 Order as In House from Job View 2022-04-25 14:40:23 -07:00
Patrick Fic
cbad157ded Merged in release/2022-04-22 (pull request #454)
Release/2022 04 22
2022-04-24 20:29:31 +00:00
102 changed files with 31597 additions and 4901 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1">
<babeledit_project be_version="2.7.1" version="1.2">
<!--
BabelEdit project file
@@ -3586,6 +3586,27 @@
<folder_node>
<name>fields</name>
<children>
<concept_node>
<name>ReceivableCustomField</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>address1</name>
<definition_loaded>false</definition_loaded>
@@ -4535,6 +4556,69 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>localmediaserverhttp</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>localmediaservernetwork</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>localmediatoken</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>logo_img_footer_margin</name>
<definition_loaded>false</definition_loaded>
@@ -8261,6 +8345,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>uselocalmediaserver</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>website</name>
<definition_loaded>false</definition_loaded>
@@ -8927,6 +9032,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>md_to_emails</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>md_to_emails_emails</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>messagingpresets</name>
<definition_loaded>false</definition_loaded>
@@ -13211,6 +13358,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>openinexplorer</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>reassign_limitexceeded</name>
<definition_loaded>false</definition_loaded>
@@ -13996,6 +14164,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>external_id</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>first_name</name>
<definition_loaded>false</definition_loaded>
@@ -14493,6 +14682,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>priorsuccesfulexport</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>
@@ -31558,6 +31768,27 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>addtorelatedro</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>newnoteplaceholder</name>
<definition_loaded>false</definition_loaded>
@@ -32395,6 +32626,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>orderinhouse</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>
@@ -36632,6 +36884,32 @@
</concept_node>
</children>
</folder_node>
<folder_node>
<name>special</name>
<children>
<concept_node>
<name>attendance_detail_csv</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>
<name>subjects</name>
<children>
@@ -38410,6 +38688,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>credits_not_received_date_vendorid</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>csi</name>
<definition_loaded>false</definition_loaded>

View File

@@ -34,6 +34,7 @@
"markerjs2": "^2.21.1",
"moment-business-days": "^1.2.0",
"moment-timezone": "^0.5.34",
"normalize-url": "^7.0.3",
"phone": "^3.1.15",
"preval.macro": "^5.0.0",
"prop-types": "^15.8.1",

View File

@@ -13,6 +13,7 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -27,7 +28,7 @@ export default connect(
mapDispatchToProps
)(AccountingPayablesTableComponent);
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
export function AccountingPayablesTableComponent({ bodyshop, loading, bills, refetch }) {
const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false);
@@ -131,11 +132,9 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
},
{
title: t("general.labels.actions"),
@@ -150,6 +149,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
disabled={transInProgress || !!record.exported}
loadingCallback={setTransInProgress}
setSelectedBills={setSelectedBills}
refetch={refetch}
/>
</div>
),
@@ -182,6 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
disabled={transInProgress || selectedBills.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedBills}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />

View File

@@ -13,6 +13,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -31,6 +32,7 @@ export function AccountingPayablesTableComponent({
bodyshop,
loading,
payments,
refetch,
}) {
const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]);
@@ -130,11 +132,9 @@ export function AccountingPayablesTableComponent({
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
},
{
title: t("general.labels.actions"),
@@ -148,6 +148,7 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || !!record.exportedat}
loadingCallback={setTransInProgress}
setSelectedPayments={setSelectedPayments}
refetch={refetch}
/>
),
},
@@ -188,6 +189,7 @@ export function AccountingPayablesTableComponent({
disabled={transInProgress || selectedPayments.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedPayments}
refetch={refetch}
/>
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
<QboAuthorizeComponent />

View File

@@ -14,6 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import { DateFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -30,6 +31,7 @@ export function AccountingReceivablesTableComponent({
bodyshop,
loading,
jobs,
refetch,
}) {
const { t } = useTranslation();
const [selectedJobs, setSelectedJobs] = useState([]);
@@ -139,12 +141,9 @@ export function AccountingReceivablesTableComponent({
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
key: "attempts",
render: (text, record) => {
const success = record.exportlogs.filter((e) => e.successful).length;
const attempts = record.exportlogs.length;
return `${success}/${attempts}`;
},
render: (text, record) => (
<ExportLogsCountDisplay logs={record.exportlogs} />
),
},
{
title: t("general.labels.actions"),
@@ -157,6 +156,7 @@ export function AccountingReceivablesTableComponent({
jobId={record.id}
disabled={!!record.date_exported}
setSelectedJobs={setSelectedJobs}
refetch={refetch}
/>
<Link to={`/manage/jobs/${record.id}/close`}>
<Button>{t("jobs.labels.viewallocations")}</Button>
@@ -207,6 +207,7 @@ export function AccountingReceivablesTableComponent({
disabled={transInProgress || selectedJobs.length === 0}
loadingCallback={setTransInProgress}
completedCallback={setSelectedJobs}
refetch={refetch}
/>
)}
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (

View File

@@ -12,27 +12,29 @@ import moment from "moment";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useHistory } from "react-router-dom";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import {
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 JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
@@ -49,6 +51,7 @@ export default connect(
export function BillDetailEditcontainer({
setPartsOrderContext,
insertAuditTrail,
bodyshop,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
@@ -265,12 +268,21 @@ export function BillDetailEditcontainer({
layout="vertical"
>
<BillFormContainer form={form} billEdit disabled={exported} />
<JobDocumentsGallery
jobId={data ? data.bills_by_pk.jobid : null}
billId={search.billid}
documentsList={data ? data.bills_by_pk.documents : []}
billsCallback={refetch}
/>
{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>
</>
)}

View File

@@ -24,6 +24,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import BillFormContainer from "../bill-form/bill-form.container";
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
import { handleUpload } from "../documents-upload/documents-upload.utility";
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
const mapStateToProps = createStructuredSelector({
billEnterModal: selectBillEnterModal,
@@ -210,19 +211,33 @@ function BillEnterModalContainer({
/////////////////////////
if (upload && upload.length > 0) {
//insert Each of the documents?
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
if (bodyshop.uselocalmediaserver) {
upload.forEach((u) => {
handleLocalUpload({
ev: { file: u.originFileObj },
context: {
jobid: values.jobid,
invoice_number: remainingValues.invoice_number,
vendorid: remainingValues.vendorid,
},
});
});
} else {
upload.forEach((u) => {
handleUpload(
{ file: u.originFileObj },
{
bodyshop: bodyshop,
uploaded_by: currentUser.email,
jobId: values.jobid,
billId: billId,
tagsArray: null,
callback: null,
}
);
});
}
}
///////////////////////////
setLoading(false);

View File

@@ -6,12 +6,13 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -19,6 +20,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
export function ChatMediaSelector({
bodyshop,
selectedMedia,
setSelectedMedia,
conversation,
@@ -27,7 +29,6 @@ export function ChatMediaSelector({
const [visible, setVisible] = useState(false);
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
@@ -66,6 +67,8 @@ export function ChatMediaSelector({
</div>
);
if (bodyshop.uselocalmediaserver) return null;
return (
<Popover
content={

View File

@@ -19,7 +19,7 @@ export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
const menu = (
<Menu>
{bodyshop.md_messaging_presets.map((i, idx) => (
<Menu.Item onClick={() => setMessage(i.text)} onItemHover key={idx}>
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
{i.label}
</Menu.Item>
))}

View File

@@ -0,0 +1,70 @@
import { UploadOutlined } from "@ant-design/icons";
import { Upload } from "antd";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { handleUpload } from "./documents-local-upload.utility";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
bodyshop: selectBodyshop,
});
export function DocumentsLocalUploadComponent({
children,
currentUser,
bodyshop,
job,
vendorid,
invoice_number,
callbackAfterUpload,
}) {
const [fileList, setFileList] = useState([]);
const handleDone = (uid) => {
setTimeout(() => {
setFileList((fileList) => fileList.filter((x) => x.uid !== uid));
}, 2000);
};
return (
<Upload.Dragger
multiple={true}
fileList={fileList}
onChange={(f) => {
if (f.event && f.event.percent === 100) handleDone(f.file.uid);
setFileList(f.fileList);
}}
customRequest={(ev) =>
handleUpload({
ev,
context: {
jobid: job.id,
vendorid,
invoice_number,
callback: callbackAfterUpload,
},
})
}
accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx"
>
{children || (
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">
Click or drag files to this area to upload.
</p>
</>
)}
</Upload.Dragger>
);
}
export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent);

View File

@@ -0,0 +1,68 @@
import cleanAxios from "../../utils/CleanAxios";
import { store } from "../../redux/store";
import { addMediaForJob } from "../../redux/media/media.actions";
import normalizeUrl from "normalize-url";
export const handleUpload = async ({ ev, context }) => {
const { onError, onSuccess, onProgress, file } = ev;
const { jobid, invoice_number, vendorid, callbackAfterUpload } = context;
const bodyshop = store.getState().user.bodyshop;
var options = {
headers: {
"X-Requested-With": "XMLHttpRequest",
ims_token: bodyshop.localmediatoken,
},
onUploadProgress: (e) => {
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
},
};
const formData = new FormData();
formData.append("jobid", jobid);
if (invoice_number) {
formData.append("invoice_number", invoice_number);
formData.append("vendorid", vendorid);
}
formData.append("file", file);
const imexMediaServerResponse = await cleanAxios.post(
normalizeUrl(
`${bodyshop.localmediaserverhttp}/${
invoice_number ? "bills" : "jobs"
}/upload`
),
formData,
{
...options,
}
);
if (imexMediaServerResponse.status !== 200) {
if (!!onError) {
onError(imexMediaServerResponse.statusText);
}
} else {
onSuccess && onSuccess(file);
store.dispatch(
addMediaForJob({
jobid,
media: imexMediaServerResponse.data.map((d) => {
return {
...d,
selected: false,
src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`),
thumbnail: normalizeUrl(
`${bodyshop.localmediaserverhttp}/${d.thumbnail}`
),
};
}),
})
);
}
if (callbackAfterUpload) {
callbackAfterUpload();
}
};

View File

@@ -9,6 +9,7 @@ import {
Space,
Menu,
Dropdown,
Button,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -20,10 +21,13 @@ import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import { selectEmailConfig } from "../../redux/email/email.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
emailConfig: selectEmailConfig,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
@@ -34,6 +38,7 @@ export default connect(
)(EmailOverlayComponent);
export function EmailOverlayComponent({
emailConfig,
form,
selectedMediaState,
bodyshop,
@@ -42,7 +47,12 @@ export function EmailOverlayComponent({
const { t } = useTranslation();
const handleClick = ({ item, key, keyPath }) => {
const email = item.props.value;
form.setFieldsValue({ to: _.uniq([...form.getFieldValue("to"), email]) });
form.setFieldsValue({
to: _.uniq([
...form.getFieldValue("to"),
...(typeof email === "string" ? [email] : email),
]),
});
};
const menu = (
@@ -55,6 +65,11 @@ export function EmailOverlayComponent({
{`${e.first_name} ${e.last_name}`}
</Menu.Item>
))}
{bodyshop.md_to_emails.map((e, idx) => (
<Menu.Item value={e.emails} key={idx + "group"}>
{e.label}
</Menu.Item>
))}
</Menu>
</div>
);
@@ -143,10 +158,17 @@ export function EmailOverlayComponent({
</Form.Item>
<Tabs>
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
</Tabs.TabPane>
{!bodyshop.uselocalmediaserver && (
<Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
<EmailDocumentsComponent selectedMediaState={selectedMediaState} />
</Tabs.TabPane>
)}
<Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
{bodyshop.uselocalmediaserver && emailConfig.jobid && (
<a href={CreateExplorerLinkForJob({ jobid: emailConfig.jobid })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
)}
<Form.Item
name="fileList"
valuePropName="fileList"

View File

@@ -0,0 +1,25 @@
import React from "react";
import { WarningOutlined } from "@ant-design/icons";
import { Space, Tooltip } from "antd";
import { useTranslation } from "react-i18next";
const style = {
fontWeight: "bold",
color: "green",
};
export default function ExportLogsCountDisplay({ logs }) {
const success = logs.filter((e) => e.successful).length;
const attempts = logs.length;
const { t } = useTranslation();
return (
<Space style={success > 0 ? style : {}}>
{`${success}/${attempts}`}
{success > 0 && (
<Tooltip title={t("exportlogs.labels.priorsuccesfulexport")}>
<WarningOutlined />
</Tooltip>
)}
</Space>
);
}

View File

@@ -6,8 +6,10 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { QUERY_JOB_CARD_DETAILS } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobSyncButton from "../job-sync-button/job-sync-button.component";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
@@ -20,6 +22,10 @@ import JobDetailCardsNotesComponent from "./job-detail-cards.notes.component";
import JobDetailCardsPartsComponent from "./job-detail-cards.parts.component";
import JobDetailCardsTotalsComponent from "./job-detail-cards.totals.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })),
@@ -31,7 +37,7 @@ const span = {
lg: { span: 8 },
};
export function JobDetailCards({ setPrintCenterContext }) {
export function JobDetailCards({ bodyshop, setPrintCenterContext }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
@@ -143,12 +149,14 @@ export function JobDetailCards({ setPrintCenterContext }) {
data={data ? data.jobs_by_pk : null}
/>
</Col>
<Col {...span}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Col>
{!bodyshop.uselocalmediaserver && (
<Col {...span}>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
</Col>
)}
<Col {...span}>
<JobDetailCardsDamageComponent
loading={loading}
@@ -161,4 +169,4 @@ export function JobDetailCards({ setPrintCenterContext }) {
</Drawer>
);
}
export default connect(null, mapDispatchToProps)(JobDetailCards);
export default connect(mapStateToProps, mapDispatchToProps)(JobDetailCards);

View File

@@ -6,6 +6,7 @@ import {
EditFilled,
PlusCircleTwoTone,
MinusCircleTwoTone,
HomeOutlined,
} from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import {
@@ -42,6 +43,7 @@ import _ from "lodash";
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -54,6 +56,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
setPartsOrderContext: (context) =>
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
setBillEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "billEnter" })),
});
export function JobLinesComponent({
@@ -68,6 +72,7 @@ export function JobLinesComponent({
job,
setJobLineEditContext,
form,
setBillEnterContext,
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
@@ -386,6 +391,62 @@ export function JobLinesComponent({
</Space>
</Tag>
)}
<Button
disabled={
(job && !job.converted) ||
(selectedLines.length > 0 ? false : true) ||
jobRO ||
technician
}
onClick={() => {
// setPartsOrderContext({
// actions: { refetch: refetch },
// context: {
// jobId: job.id,
// job: job,
// linesToOrder: selectedLines,
// },
// });
setBillEnterContext({
actions: { refetch: refetch },
context: {
disableInvNumber: true,
job: { id: job.id },
bill: {
vendorid: bodyshop.inhousevendorid,
invoice_number: "ih",
isinhouse: true,
date: new moment(),
total: 0,
billlines: selectedLines.map((p) => {
return {
joblineid: p.id,
actual_price: p.act_price,
actual_cost: 0, //p.act_price,
line_desc: p.line_desc,
line_remarks: p.line_remarks,
part_type: p.part_type,
quantity: p.quantity || 1,
applicable_taxes: {
local: false,
state: false,
federal: false,
},
};
}),
},
},
});
//Clear out the selected lines. IO-785
setSelectedLines([]);
}}
>
<HomeOutlined />
{t("parts.actions.orderinhouse")}
{selectedLines.length > 0 && ` (${selectedLines.length})`}
</Button>
<Button
disabled={
(job && !job.converted) ||

View File

@@ -24,7 +24,7 @@ export function JoblinePresetButton({ bodyshop, form }) {
const menu = (
<Menu>
{bodyshop.md_jobline_presets.map((i, idx) => (
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
{i.label}
</Menu.Item>
))}

View File

@@ -141,7 +141,9 @@ export function JobLinesUpsertModalComponent({
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (!!getFieldValue("mod_lbr_ty") === !!value) {
if (
!!getFieldValue("mod_lbr_ty") === (!!value || value === 0)
) {
return Promise.resolve();
}
return Promise.reject(

View File

@@ -15,7 +15,6 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
import { useHistory } from "react-router-dom";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
@@ -27,6 +26,7 @@ export function JobsCloseExportButton({
jobId,
disabled,
setSelectedJobs,
refetch,
}) {
const history = useHistory();
const { t } = useTranslation();
@@ -46,13 +46,10 @@ export function JobsCloseExportButton({
//Check if it's a QBO Setup.
let PartnerResponse;
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(
`/qbo/receivables`,
{
jobIds: [jobId],
},
);
PartnerResponse = await axios.post(`/qbo/receivables`, {
jobIds: [jobId],
elgen: true,
});
} else {
//Default is QBD
@@ -117,58 +114,64 @@ export function JobsCloseExportButton({
});
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
}
} else {
//Insert success export log.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: jobId,
successful: true,
useremail: currentUser.email,
},
],
},
},
});
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
const jobUpdateResponse = await updateJob({
variables: {
jobId: jobId,
job: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
},
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
if (setSelectedJobs) {
setSelectedJobs((selectedJobs) => {
@@ -176,7 +179,7 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -146,7 +146,11 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel forceRender key="claim" header={t("menus.jobsdetail.claimdetail")}>
<Collapse.Panel
forceRender
key="claim"
header={t("menus.jobsdetail.claimdetail")}
>
<LayoutFormRow>
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
<Input />
@@ -193,7 +197,8 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
</Form.Item>
</LayoutFormRow>
</Collapse.Panel>
<Collapse.Panel forceRender
<Collapse.Panel
forceRender
key="financial"
header={t("menus.jobsdetail.financials")}
>
@@ -204,7 +209,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<CurrencyInput min={0} />
</Form.Item>
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
<Select>
<Select allowClear>
<Select.Option value="W">
{t("jobs.labels.deductible.waived")}
</Select.Option>

View File

@@ -1,5 +1,5 @@
import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons";
import { Card, Col, Row, Space, Button } from "antd";
import { EditFilled, FileExcelFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Col, Row, Space } from "antd";
import React, { useEffect, useState } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";

View File

@@ -0,0 +1,107 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Space } from "antd";
import React, { useEffect } from "react";
import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
getBillMedia,
getJobMedia,
toggleMediaSelected,
} from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component";
import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component";
import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
allMedia: selectAllMedia,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
getBillMedia: ({ jobid, invoice_number }) => {
dispatch(getBillMedia({ jobid, invoice_number }));
},
toggleMediaSelected: ({ jobid, filename }) =>
dispatch(toggleMediaSelected({ jobid, filename })),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGallery);
export function JobsDocumentsLocalGallery({
bodyshop,
toggleMediaSelected,
getJobMedia,
getBillMedia,
allMedia,
job,
invoice_number,
vendorid,
}) {
const { t } = useTranslation();
useEffect(() => {
if (job) {
if (invoice_number) {
getBillMedia({ jobid: job.id, invoice_number });
} else {
getJobMedia(job.id);
}
}
}, [job, invoice_number, getJobMedia, getBillMedia]);
return (
<div>
<Space wrap>
<Button
onClick={() => {
if (job) {
if (invoice_number) {
getBillMedia({ jobid: job.id, invoice_number });
} else {
getJobMedia(job.id);
}
}
}}
>
<SyncOutlined />
</Button>
<a href={CreateExplorerLinkForJob({ jobid: job.id })}>
<Button>{t("documents.labels.openinexplorer")}</Button>
</a>
<JobsDocumentsLocalGalleryReassign jobid={job.id} />
<JobsDocumentsLocalGallerySelectAllComponent jobid={job.id} />
</Space>
<Card>
<DocumentsLocalUploadComponent
job={job}
invoice_number={invoice_number}
vendorid={vendorid}
/>
</Card>
<Card title={t("jobs.labels.documents-images")}>
<Gallery
images={(allMedia && allMedia[job.id]) || []}
backdropClosesModal={true}
onSelectImage={(index, image) => {
toggleMediaSelected({ jobid: job.id, filename: image.filename });
}}
onClickImage={(props) => {
window.open(
props.target.src,
"_blank",
"toolbar=0,location=0,menubar=0"
);
}}
/>
</Card>
</div>
);
}

View File

@@ -0,0 +1,96 @@
import { Button, Form, Popover, Space } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { getJobMedia } from "../../redux/media/media.actions";
import { selectAllMedia } from "../../redux/media/media.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import cleanAxios from "../../utils/CleanAxios";
import JobSearchSelect from "../job-search-select/job-search-select.component";
const mapStateToProps = createStructuredSelector({
allMedia: selectAllMedia,
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
getJobMedia: (id) => dispatch(getJobMedia(id)),
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGalleryReassign);
export function JobsDocumentsLocalGalleryReassign({
bodyshop,
jobid,
allMedia,
getJobMedia,
}) {
const { t } = useTranslation();
const [form] = Form.useForm();
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const handleFinish = async ({ jobid: newJobid }) => {
setLoading(true);
const selectedDocuments = allMedia[jobid].filter((m) => m.isSelected);
await cleanAxios.post(
`${bodyshop.localmediaserverhttp}/jobs/move`,
{
from_jobid: jobid,
jobid: newJobid,
files: selectedDocuments.map((f) => f.filename),
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
getJobMedia(jobid);
setVisible(false);
setLoading(false);
};
const popContent = (
<div>
<Form onFinish={handleFinish} layout="vertical" form={form}>
<Form.Item
label={t("documents.labels.newjobid")}
style={{ width: "20rem" }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
name={"jobid"}
>
<JobSearchSelect />
</Form.Item>
</Form>
<Space>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.submit")}
</Button>
<Button onClick={() => setVisible(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</div>
);
return (
<Popover content={popContent} visible={visible}>
<Button
//disabled={selectedImages.length < 1}
onClick={() => setVisible(true)}
loading={loading}
>
{t("documents.actions.reassign")}
</Button>
</Popover>
);
}

View File

@@ -0,0 +1,53 @@
import { Button, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAllmediaForJob,
deselectAllMediaForJob,
} from "../../redux/media/media.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
selectAllmediaForJob: (jobid) => dispatch(selectAllmediaForJob(jobid)),
deselectAllmediaForJob: (jobid) => dispatch(deselectAllMediaForJob(jobid)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsDocumentsLocalGallerySelectAllComponent);
export function JobsDocumentsLocalGallerySelectAllComponent({
jobid,
selectAllmediaForJob,
deselectAllmediaForJob,
}) {
const { t } = useTranslation();
// onSelectImage={(index, image) => {
// toggleMediaSelected({ jobid: job.id, filename: image.filename });
// }}
const handleSelectAll = () => {
selectAllmediaForJob({ jobid });
};
const handleDeselectAll = () => {
deselectAllmediaForJob({ jobid });
};
return (
<Space wrap>
<Button onClick={handleSelectAll}>
{t("general.actions.selectall")}
</Button>
<Button onClick={handleDeselectAll}>
{t("general.actions.deselectall")}
</Button>
</Space>
);
}

View File

@@ -26,6 +26,7 @@ export function JobsExportAllButton({
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS);
@@ -39,6 +40,7 @@ export function JobsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/receivables`, {
jobIds: jobIds,
elgen: true,
});
} else {
let QbXmlResponse;
@@ -83,6 +85,7 @@ export function JobsExportAllButton({
return;
}
}
console.log("PartnerResponse", PartnerResponse);
const groupedData = _.groupBy(
PartnerResponse.data,
@@ -106,61 +109,70 @@ export function JobsExportAllButton({
});
//Call is not awaited as it is not critical to finish before proceeding.
});
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const jobUpdateResponse = await updateJob({
variables: {
jobIds: [key],
fields: {
status: bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
},
});
});
}
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
jobid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
const jobUpdateResponse = await updateJob({
variables: {
jobIds: [key],
fields: {
status:
bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date(),
},
},
});
if (!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
}
})
);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);

View File

@@ -61,6 +61,7 @@ export function JobNotesContainer({ jobId, insertAuditTrail }) {
jobId={jobId}
loading={loading}
data={data ? data.jobs_by_pk.notes : null}
relatedRos={data ? data.jobs_by_pk.vehicle.jobs : null}
refetch={refetch}
deleteLoading={deleteLoading}
handleNoteDelete={handleNoteDelete}

View File

@@ -37,6 +37,7 @@ export function JobNotesComponent({
setNoteUpsertContext,
deleteLoading,
ro_number,
relatedRos,
}) {
const { t } = useTranslation();
const Templates = TemplateList("job_special", {
@@ -149,6 +150,7 @@ export function JobNotesComponent({
actions: { refetch: refetch },
context: {
jobId: jobId,
relatedRos: relatedRos,
},
});
}}

View File

@@ -1,51 +1,92 @@
import { Col, Form, Input, Row, Switch } from "antd";
import { Checkbox, Col, Form, Input, Row, Space, Switch, Tag } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import NotesPresetButton from "../notes-preset-button/notes-preset-button.component";
export default function NoteUpsertModalComponent({ form }) {
const mapStateToProps = createStructuredSelector({
noteUpsertModal: selectNoteUpsert,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(NoteUpsertModalComponent);
export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
const { t } = useTranslation();
const { jobId, existingNote, relatedRos } = noteUpsertModal.context;
const filteredRelatedRos = relatedRos
? relatedRos.filter((j) => j.id !== jobId)
: [];
return (
<Row gutter={[16, 16]}>
<Col span={8}>
<Form.Item
label={t("notes.fields.critical")}
name="critical"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={t("notes.fields.private")}
name="private"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<NotesPresetButton form={form} />
</Col>
<Col span={24}>
<Form.Item
label={t("notes.fields.text")}
name="text"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input.TextArea
rows={8}
placeholder={t("notes.labels.newnoteplaceholder")}
/>
</Form.Item>
</Col>
</Row>
<>
<Row gutter={[16, 16]}>
<Col span={8}>
<Form.Item
label={t("notes.fields.critical")}
name="critical"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label={t("notes.fields.private")}
name="private"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Col>
<Col span={8}>
<NotesPresetButton form={form} />
</Col>
<Col span={24}>
<Form.Item
label={t("notes.fields.text")}
name="text"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<Input.TextArea
rows={8}
placeholder={t("notes.labels.newnoteplaceholder")}
/>
</Form.Item>
</Col>
</Row>
<div>
<div>{!existingNote && t("notes.labels.addtorelatedro")}</div>
{!existingNote &&
filteredRelatedRos.map((j, idx) => (
<Space key={j.id} align="center">
<Form.Item
noStyle
name={["relatedros", j.id]}
valuePropName="checked"
>
<Checkbox />
</Form.Item>
<Tag>
{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${
j.status ? ` | ${j.status}` : ""
}`}
</Tag>
</Space>
))}
</div>
</>
);
}

View File

@@ -4,14 +4,14 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
import { selectCurrentUser } from "../../redux/user/user.selectors";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import NoteUpsertModalComponent from "./note-upsert-modal.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -48,7 +48,9 @@ export function NoteUpsertModalContainer({
}
}, [existingNote, form, visible]);
const handleFinish = (values) => {
const handleFinish = async (formValues) => {
const { relatedros, ...values } = formValues;
if (existingNote) {
logImEXEvent("job_note_update");
@@ -70,24 +72,44 @@ export function NoteUpsertModalContainer({
toggleModalVisible();
} else {
logImEXEvent("job_note_insert");
const AdditionalNoteInserts = relatedros
? Object.keys(relatedros).filter((key) => relatedros[key])
: [];
insertNote({
await insertNote({
variables: {
noteInput: [
{ ...values, jobid: jobId, created_by: currentUser.email },
],
},
}).then((r) => {
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("notes.successes.create"),
});
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
});
if (AdditionalNoteInserts.length > 0) {
//Insert the others.
AdditionalNoteInserts.forEach(async (newJobId) => {
await insertNote({
variables: {
noteInput: [
{ ...values, jobid: newJobId, created_by: currentUser.email },
],
},
});
insertAuditTrail({
jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(),
});
});
}
if (refetch) refetch();
form.resetFields();
toggleModalVisible();
notification["success"]({
message: t("notes.successes.create"),
});
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
});
}
};

View File

@@ -24,7 +24,7 @@ export function NotesPresetButton({ bodyshop, form }) {
const menu = (
<Menu>
{bodyshop.md_notes_presets.map((i, idx) => (
<Menu.Item onClick={() => handleSelect(i)} onItemHover key={idx}>
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
{i.label}
</Menu.Item>
))}

View File

@@ -0,0 +1,47 @@
import React from "react";
import { Button, Popconfirm } from "antd";
import { DeleteFilled } from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { DELETE_PARTS_ORDER_LINE } from "../../graphql/parts-orders.queries";
import { useMutation } from "@apollo/client";
export default function PartsOrderDeleteLine({
disabled,
partsLineId,
partsOrderId,
}) {
const { t } = useTranslation();
const [deletePartsOrderLine] = useMutation(DELETE_PARTS_ORDER_LINE);
return (
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={disabled}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrderLine({
variables: { partsOrderLineId: partsLineId },
update(cache) {
cache.modify({
id: cache.identify({
__typename: "parts_orders",
id: partsOrderId,
}),
fields: {
parts_order_lines(cached, { readField }) {
return cached.filter((c) => {
return readField("id", c) !== partsLineId;
});
},
},
});
},
});
}}
>
<Button disabled={disabled}>
<DeleteFilled />
</Button>
</Popconfirm>
);
}

View File

@@ -30,6 +30,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
import DataLabel from "../data-label/data-label.component";
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
@@ -391,12 +392,21 @@ export function PartsOrderListTableComponent({
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
<Space wrap>
<PartsOrderDeleteLine
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
partsOrderId={selectedpartsorder}
jobLineId={record.job_line_id}
/>
<PartsOrderLineBackorderButton
disabled={jobRO}
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</Space>
),
},
];

View File

@@ -27,6 +27,7 @@ export function PayableExportAll({
disabled,
loadingCallback,
completedCallback,
refetch,
}) {
const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS);
@@ -42,6 +43,7 @@ export function PayableExportAll({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, {
bills: billids,
elgen: true,
});
} else {
let QbXmlResponse;
@@ -104,57 +106,62 @@ export function PayableExportAll({
}),
})
);
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
}
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: [key],
bill: {
exported: true,
exported_at: new Date(),
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
}
}
})()
@@ -164,6 +171,8 @@ export function PayableExportAll({
await Promise.all(proms);
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -26,6 +26,7 @@ export function PayableExportButton({
disabled,
loadingCallback,
setSelectedBills,
refetch,
}) {
const { t } = useTranslation();
const [updateBill] = useMutation(UPDATE_BILLS);
@@ -43,6 +44,7 @@ export function PayableExportButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payables`, {
bills: [billId],
elgen: true,
});
} else {
//Default is QBD
@@ -100,64 +102,72 @@ export function PayableExportButton({
}),
})
);
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
}
if (successfulTransactions.length > 0) {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: {
exported: true,
exported_at: new Date(),
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
}
if (successfulTransactions.length > 0) {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
billid: billId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const billUpdateResponse = await updateBill({
variables: {
billIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "billid"
: "id"
]
),
bill: {
exported: true,
exported_at: new Date(),
},
},
});
if (!!!billUpdateResponse.errors) {
notification.open({
type: "success",
key: "billsuccessexport",
message: t("bills.successes.exported"),
});
} else {
notification["error"]({
message: t("bills.errors.exporting", {
error: JSON.stringify(billUpdateResponse.error),
}),
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (setSelectedBills) {
setSelectedBills((selectedBills) => {
return selectedBills.filter((i) => i !== billId);

View File

@@ -26,6 +26,7 @@ export function PaymentExportButton({
disabled,
loadingCallback,
setSelectedPayments,
refetch,
}) {
const { t } = useTranslation();
const [updatePayment] = useMutation(UPDATE_PAYMENTS);
@@ -40,6 +41,7 @@ export function PaymentExportButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, {
payments: [paymentId],
elgen: true,
});
} else {
//Default is QBD
@@ -100,63 +102,68 @@ export function PaymentExportButton({
}),
})
);
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: {
exportedat: new Date(),
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
}
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: paymentId,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayment({
variables: {
paymentIdList: successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "paymentid"
: "id"
]
),
payment: {
exportedat: new Date(),
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
}
if (setSelectedPayments) {
@@ -165,7 +172,7 @@ export function PaymentExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
if (!!loadingCallback) loadingCallback(false);
setLoading(false);
};

View File

@@ -25,6 +25,7 @@ export function PaymentsExportAllButton({
disabled,
loadingCallback,
completedCallback,
refetch
}) {
const { t } = useTranslation();
const [updatePayments] = useMutation(UPDATE_PAYMENTS);
@@ -38,6 +39,7 @@ export function PaymentsExportAllButton({
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
PartnerResponse = await axios.post(`/qbo/payments`, {
payments: paymentIds,
elgen: true,
});
} else {
let QbXmlResponse;
@@ -92,54 +94,61 @@ export function PaymentsExportAllButton({
}),
})
);
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
});
} else {
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: [key],
payment: {
exportedat: new Date(),
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: false,
message: JSON.stringify(
failedTransactions.map((ft) => ft.errorMessage)
),
useremail: currentUser.email,
},
],
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
}
} else {
if (!(bodyshop.accountingconfig && bodyshop.accountingconfig.qbo)) {
//QBO Logs are handled server side.
await insertExportLog({
variables: {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: key,
successful: true,
useremail: currentUser.email,
},
],
},
});
const paymentUpdateResponse = await updatePayments({
variables: {
paymentIdList: [key],
payment: {
exportedat: new Date(),
},
},
});
if (!!!paymentUpdateResponse.errors) {
notification.open({
type: "success",
key: "paymentsuccessexport",
message: t("payments.successes.exported"),
});
} else {
notification["error"]({
message: t("payments.errors.exporting", {
error: JSON.stringify(paymentUpdateResponse.error),
}),
});
}
}
}
})()
@@ -148,6 +157,7 @@ export function PaymentsExportAllButton({
await Promise.all(proms);
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) refetch();
setLoading(false);
};

View File

@@ -22,9 +22,10 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setModalContext } from "../../redux/modals/modals.actions";
import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
@@ -35,7 +36,11 @@ export default connect(
mapDispatchToProps
)(ProductionListDetail);
export function ProductionListDetail({ jobs, setPrintCenterContext }) {
export function ProductionListDetail({
bodyshop,
jobs,
setPrintCenterContext,
}) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const { selected } = search;
@@ -144,11 +149,12 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) {
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
{!bodyshop.uselocalmediaserver && (
<JobDetailCardsDocumentsComponent
loading={loading}
data={data ? data.jobs_by_pk : null}
/>
)}
</div>
)}
</Drawer>

View File

@@ -328,6 +328,12 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
>
<Input />
</Form.Item>
<Form.Item
label={t("employees.fields.external_id")}
name="external_id"
>
<Input />
</Form.Item>
</LayoutFormRow>
<Form.List name={["rates"]}>
{(fields, { add, remove, move }) => {

View File

@@ -352,10 +352,27 @@ export default function ShopInfoGeneral({ form }) {
>
<Switch />
</Form.Item>
<Form.Item
name={["accountingconfig", "ReceivableCustomField1"]}
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
>
{ReceivableCustomFieldSelect}
</Form.Item>
<Form.Item
name={["accountingconfig", "ReceivableCustomField2"]}
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
>
{ReceivableCustomFieldSelect}
</Form.Item>
<Form.Item
name={["accountingconfig", "ReceivableCustomField3"]}
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
>
{ReceivableCustomFieldSelect}
</Form.Item>
<Form.Item
name={["md_classes"]}
label={t("bodyshop.fields.md_classes")}
dependencies={["enforce_class"]}
rules={[
({ getFieldValue }) => {
return {
@@ -584,6 +601,31 @@ export default function ShopInfoGeneral({ form }) {
>
<Switch />
</Form.Item>
<Form.Item
name={["uselocalmediaserver"]}
label={t("bodyshop.fields.uselocalmediaserver")}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={["localmediaserverhttp"]}
label={t("bodyshop.fields.localmediaserverhttp")}
>
<Input />
</Form.Item>
<Form.Item
name={["localmediaservernetwork"]}
label={t("bodyshop.fields.localmediaservernetwork")}
>
<Input />
</Form.Item>
<Form.Item
name={["localmediatoken"]}
label={t("bodyshop.fields.localmediatoken")}
>
<Input />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.messagingpresets")}>
<Form.List name={["md_messaging_presets"]}>
@@ -682,7 +724,7 @@ export default function ShopInfoGeneral({ form }) {
},
]}
>
<Input />
<Input.TextArea rows={3} />
</Form.Item>
<Space wrap>
<DeleteFilled
@@ -1393,6 +1435,68 @@ export default function ShopInfoGeneral({ form }) {
}}
</Form.List>
</LayoutFormRow>
<LayoutFormRow grow header={t("bodyshop.labels.md_to_emails")}>
<Form.List name={["md_to_emails"]}>
{(fields, { add, remove, move }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<LayoutFormRow noDivider>
<Form.Item
label={t("general.labels.label")}
key={`${index}label`}
name={[field.name, "label"]}
>
<Input />
</Form.Item>
<Form.Item
label={t("bodyshop.labels.md_to_emails_emails")}
key={`${index}emails`}
name={[field.name, "emails"]}
>
<Select mode="tags" tokenSeparators={[",", ";"]} />
</Form.Item>
<Space>
<DeleteFilled
onClick={() => {
remove(field.name);
}}
/>
<FormListMoveArrows
move={move}
index={index}
total={fields.length}
/>
</Space>
</LayoutFormRow>
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("general.actions.add")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</LayoutFormRow>
</div>
);
}
const ReceivableCustomFieldSelect = (
<Select>
<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>
</Select>
);

View File

@@ -6,15 +6,20 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component";
import JobLinesContainer from "../job-detail-lines/job-lines.container";
import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
import JobsDocumentsGalleryContainer from "../jobs-documents-gallery/jobs-documents-gallery.container";
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import JobNotesContainer from "../jobs-notes/jobs-notes.container";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop });
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "printCenter" })),
@@ -29,7 +34,7 @@ const mapDispatchToProps = (dispatch) => ({
// },
// };
export function JobDetailCards({ setPrintCenterContext }) {
export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
.filter((screen) => !!screen[1])
.slice(-1)[0];
@@ -110,7 +115,13 @@ export function JobDetailCards({ setPrintCenterContext }) {
/>
</Tabs.TabPane>
<Tabs.TabPane key="documents" tab={t("jobs.labels.documents")}>
<JobsDocumentsGalleryContainer jobId={searchParams.selected} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery
job={data ? data.jobs_by_pk : null}
/>
) : (
<JobsDocumentsGalleryContainer jobId={searchParams.selected} />
)}
</Tabs.TabPane>
<Tabs.TabPane key="notes" tab={t("jobs.labels.notes")}>
<JobNotesContainer jobId={searchParams.selected} />
@@ -121,4 +132,7 @@ export function JobDetailCards({ setPrintCenterContext }) {
</Drawer>
);
}
export default connect(null, mapDispatchToProps)(JobDetailCards);
export default connect(
mapStateToProps,
mapDispatchToProps
)(TechLookupJobsDrawer);

View File

@@ -237,7 +237,11 @@ export function TimeTicketModalComponent({
return Promise.reject(
t("timetickets.validation.clockoffwithoutclockon")
);
if (value && !value.isSameOrAfter(clockon))
if (
value &&
value.isSameOrAfter &&
!value.isSameOrAfter(clockon)
)
return Promise.reject(
t("timetickets.validation.clockoffmustbeafterclockon")
);

View File

@@ -0,0 +1,42 @@
import { Button } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useLocation } from "react-router-dom";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { useTranslation } from "react-i18next";
import moment from "moment";
const AttendanceCsv = TemplateList("special").attendance_detail_csv;
export default function TimeTicketsAttendanceTable() {
const searchParams = queryString.parse(useLocation().search);
const { start, end } = searchParams;
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await GenerateDocument(
{
name: AttendanceCsv.key,
variables: {
start: start
? start
: moment().startOf("week").subtract(7, "days").format("YYYY-MM-DD"),
end: end ? end : moment().endOf("week").format("YYYY-MM-DD"),
},
},
{},
"text"
);
setLoading(false);
};
return (
<Button loading={loading} onClick={handleClick}>
{t("printcenter.special.attendance_detail_csv")}
</Button>
);
}

View File

@@ -106,6 +106,11 @@ export const QUERY_BODYSHOP = gql`
last_name_first
md_parts_order_comment
bill_allow_post_to_closed
md_to_emails
uselocalmediaserver
localmediaserverhttp
localmediaservernetwork
localmediatoken
employees {
user_email
id
@@ -114,6 +119,7 @@ export const QUERY_BODYSHOP = gql`
last_name
employee_number
rates
external_id
}
}
}
@@ -209,6 +215,11 @@ export const UPDATE_SHOP = gql`
last_name_first
md_parts_order_comment
bill_allow_post_to_closed
md_to_emails
uselocalmediaserver
localmediaserverhttp
localmediaservernetwork
localmediatoken
employees {
id
first_name
@@ -217,6 +228,7 @@ export const UPDATE_SHOP = gql`
employee_number
rates
user_email
external_id
}
}
}

View File

@@ -25,6 +25,7 @@ export const QUERY_EMPLOYEE_BY_ID = gql`
rates
pin
user_email
external_id
employee_vacations(order_by: { start: desc }) {
id
start

View File

@@ -15,6 +15,14 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
jobs_by_pk(id: $id) {
id
ro_number
vehicle{
jobs{
id
ro_number
status
clm_no
}
}
notes {
created_at
created_by

View File

@@ -292,6 +292,14 @@ export const DELETE_PARTS_ORDER = gql`
}
`;
export const DELETE_PARTS_ORDER_LINE = gql`
mutation DELETE_PARTS_ORDER_LINE($partsOrderLineId: uuid!) {
delete_parts_order_lines_by_pk(id: $partsOrderLineId) {
id
}
}
`;
export const MUTATION_UPDATE_PO_CM_REECEIVED = gql`
mutation MUTATION_UPDATE_PO_CM_REECEIVED(
$partsLineId: uuid!

View File

@@ -45,7 +45,7 @@ export function AccountingPayablesContainer({
checkPartnerStatus(bodyshop, true);
}, [t, setBreadcrumbs, setSelectedHeader, bodyshop]);
const { loading, error, data } = useQuery(QUERY_BILLS_FOR_EXPORT, {
const { loading, error, data, refetch } = useQuery(QUERY_BILLS_FOR_EXPORT, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
@@ -73,6 +73,7 @@ export function AccountingPayablesContainer({
<AccountingPayablesTable
loadaing={loading}
bills={data ? data.bills : []}
refetch={refetch}
/>
</RbacWrapper>
</div>

View File

@@ -44,10 +44,13 @@ export function AccountingPaymentsContainer({
checkPartnerStatus(bodyshop, true);
}, [t, setBreadcrumbs, setSelectedHeader, bodyshop]);
const { loading, error, data } = useQuery(QUERY_PAYMENTS_FOR_EXPORT, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
});
const { loading, error, data, refetch } = useQuery(
QUERY_PAYMENTS_FOR_EXPORT,
{
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
}
);
if (error) return <AlertComponent message={error.message} type="error" />;
const noPath =
@@ -70,6 +73,7 @@ export function AccountingPaymentsContainer({
<AccountingPaymentsTable
loadaing={loading}
payments={data ? data.payments : []}
refetch={refetch}
/>
</RbacWrapper>
</div>

View File

@@ -44,7 +44,7 @@ export function AccountingReceivablesContainer({
checkPartnerStatus(bodyshop, true);
}, [t, setBreadcrumbs, setSelectedHeader, bodyshop]);
const { loading, error, data } = useQuery(QUERY_JOBS_FOR_EXPORT, {
const { loading, error, data, refetch } = useQuery(QUERY_JOBS_FOR_EXPORT, {
variables: {
invoicedStatus: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*",
},
@@ -75,6 +75,7 @@ export function AccountingReceivablesContainer({
<AccountingReceivablesTable
loadaing={loading}
jobs={data ? data.jobs : []}
refetch={refetch}
/>
</RbacWrapper>
</div>

View File

@@ -50,6 +50,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions";
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -62,6 +63,7 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertAuditTrail({ jobid, operation })),
});
export function JobsDetailPage({
bodyshop,
setPrintCenterContext,
jobRO,
job,
@@ -344,7 +346,11 @@ export function JobsDetailPage({
}
key="documents"
>
<JobsDocumentsGalleryContainer jobId={job.id} />
{bodyshop.uselocalmediaserver ? (
<JobsDocumentsLocalGallery job={job} />
) : (
<JobsDocumentsGalleryContainer jobId={job.id} />
)}
</Tabs.TabPane>
<Tabs.TabPane
tab={

View File

@@ -5,15 +5,34 @@ import JobsDocumentsComponent from "../../components/jobs-documents-gallery/jobs
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
export default function TemporaryDocsComponent() {
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(TemporaryDocsComponent);
export function TemporaryDocsComponent({ bodyshop }) {
const { loading, error, data, refetch } = useQuery(QUERY_TEMPORARY_DOCS, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
skip: bodyshop.uselocalmediaserver,
});
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
if (bodyshop.uselocalmediaserver) {
return <JobsDocumentsLocalGallery job={{ id: "temporary" }} />;
}
return (
<JobsDocumentsComponent
data={data ? data.documents : []}

View File

@@ -14,6 +14,7 @@ import TimeTicketList from "../../components/time-ticket-list/time-ticket-list.c
import TimeTicketsPayrollTable from "../../components/time-tickets-payroll-table/time-tickets-payroll-table.component";
import TimeTicketsSummaryEmployees from "../../components/time-tickets-summary-employees/time-tickets-summary-employees.component";
import { QUERY_TIME_TICKETS_IN_RANGE } from "../../graphql/timetickets.queries";
import TimeTicketsAttendanceTable from "../../components/time-tickets-attendance-table/time-tickets-attendance-table.component";
import {
setBreadcrumbs,
setSelectedHeader,
@@ -71,6 +72,7 @@ export function TimeTicketsContainer({
timetickets={data ? data.timetickets : []}
extra={
<Space wrap>
<TimeTicketsAttendanceTable />
<TimeTicketsPayrollTable />
<TimeTicketsDatesSelector />
</Space>

View File

@@ -0,0 +1,44 @@
import MediaActionTypes from "./media.types";
export const getJobMedia = (jobid) => ({
type: MediaActionTypes.GET_MEDIA_FOR_JOB,
payload: jobid,
});
export const getBillMedia = ({ jobid, invoice_number }) => {
console.log("in the action");
return {
type: MediaActionTypes.GET_MEDIA_FOR_BILL,
payload: { jobid, invoice_number },
};
};
export const setJobMedia = ({ jobid, media }) => ({
type: MediaActionTypes.SET_MEDIA_FOR_JOB,
payload: { jobid, media },
});
export const addMediaForJob = ({ jobid, media }) => ({
type: MediaActionTypes.ADD_MEDIA_FOR_JOB,
payload: { jobid, media },
});
export const getJobMediaError = ({ error, message }) => ({
type: MediaActionTypes.GET_MEDIA_FOR_JOB_ERROR,
payload: { error, message },
});
export const toggleMediaSelected = ({ jobid, filename }) => ({
type: MediaActionTypes.TOGGLE_MEDIA_SELECTED,
payload: { jobid, filename },
});
export const deselectAllMediaForJob = ({ jobid }) => ({
type: MediaActionTypes.DESELECT_ALL_MEDIA_FOR_JOB,
payload: { jobid },
});
export const selectAllmediaForJob = ({ jobid }) => ({
type: MediaActionTypes.SELECT_ALL_MEDIA_FOR_JOB,
payload: { jobid },
});

View File

@@ -0,0 +1,51 @@
import MediaActionTypes from "./media.types";
const INITIAL_STATE = { error: null };
const mediaReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case MediaActionTypes.SET_MEDIA_FOR_JOB:
return { ...state, [action.payload.jobid]: action.payload.media };
case MediaActionTypes.GET_MEDIA_FOR_JOB_ERROR:
return { ...state, error: action.payload };
case MediaActionTypes.ADD_MEDIA_FOR_JOB:
return {
...state,
[action.payload.jobid]: [
...(state[action.payload.jobid] ? state[action.payload.jobid] : []),
...(action.payload.media || []),
],
};
case MediaActionTypes.TOGGLE_MEDIA_SELECTED:
return {
...state,
[action.payload.jobid]: state[action.payload.jobid].map((p) => {
if (p.filename === action.payload.filename) {
p.isSelected = !p.isSelected;
}
return p;
}),
};
case MediaActionTypes.SELECT_ALL_MEDIA_FOR_JOB:
return {
...state,
[action.payload.jobid]: state[action.payload.jobid].map((p) => {
p.isSelected = true;
return p;
}),
};
case MediaActionTypes.DESELECT_ALL_MEDIA_FOR_JOB:
return {
...state,
[action.payload.jobid]: state[action.payload.jobid].map((p) => {
p.isSelected = false;
return p;
}),
};
default:
return state;
}
};
export default mediaReducer;

View File

@@ -0,0 +1,109 @@
import { all, call, takeLatest, put, select } from "redux-saga/effects";
import { getJobMediaError, setJobMedia } from "./media.actions";
import MediaActionTypes from "./media.types";
import cleanAxios from "../../utils/CleanAxios";
import normalizeUrl from "normalize-url";
export function* onSetJobMedia() {
yield takeLatest(MediaActionTypes.GET_MEDIA_FOR_JOB, getJobMedia);
}
export function* getJobMedia({ payload: jobid }) {
try {
const bodyshop = yield select((state) => state.user.bodyshop);
const localmediaserverhttp = bodyshop.localmediaserverhttp.trim();
if (localmediaserverhttp && localmediaserverhttp !== "") {
const imagesFetch = yield cleanAxios.post(
`${localmediaserverhttp}/jobs/list`,
{
jobid,
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
const documentsFetch = yield cleanAxios.post(
`${localmediaserverhttp}/bills/list`,
{
jobid,
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
yield put(
setJobMedia({
jobid,
media: [
...imagesFetch.data.map((d, idx) => {
return {
...d,
src: normalizeUrl(`${localmediaserverhttp}/${d.src}`),
thumbnail: normalizeUrl(
`${localmediaserverhttp}/${d.thumbnail}`
),
isSelected: false,
key: idx,
};
}),
...documentsFetch.data.map((d, idx) => {
return {
...d,
src: normalizeUrl(`${localmediaserverhttp}/${d.src}`),
thumbnail: normalizeUrl(
`${localmediaserverhttp}/${d.thumbnail}`
),
isSelected: false,
key: idx,
};
}),
],
})
);
}
} catch (error) {
yield put(getJobMediaError(error));
}
}
export function* onSetBillMedia() {
yield takeLatest(MediaActionTypes.GET_MEDIA_FOR_BILL, getBillMedia);
}
export function* getBillMedia({ payload: { jobid, invoice_number } }) {
try {
const bodyshop = yield select((state) => state.user.bodyshop);
const localmediaserverhttp = bodyshop.localmediaserverhttp.trim();
if (localmediaserverhttp && localmediaserverhttp !== "") {
const documentsFetch = yield cleanAxios.post(
`${localmediaserverhttp}/bills/list`,
{
jobid,
invoice_number,
},
{ headers: { ims_token: bodyshop.localmediatoken } }
);
yield put(
setJobMedia({
jobid,
media: [
...documentsFetch.data.map((d, idx) => {
return {
...d,
src: normalizeUrl(`${localmediaserverhttp}/${d.src}`),
thumbnail: normalizeUrl(
`${localmediaserverhttp}/${d.thumbnail}`
),
isSelected: false,
key: idx,
};
}),
],
})
);
}
} catch (error) {
yield put(getJobMediaError(error));
}
}
export function* mediaSagas() {
yield all([call(onSetJobMedia), call(onSetBillMedia)]);
}

View File

@@ -0,0 +1,5 @@
import { createSelector } from "reselect";
const selectMedia = (state) => state.media;
export const selectAllMedia = createSelector([selectMedia], (media) => media);

View File

@@ -0,0 +1,11 @@
const MediaActionTypes = {
SET_MEDIA_FOR_JOB: "SET_MEDIA_FOR_JOB",
GET_MEDIA_FOR_JOB: "GET_MEDIA_FOR_JOB",
GET_MEDIA_FOR_JOB_ERROR: "GET_MEDIA_FOR_JOB_ERROR",
ADD_MEDIA_FOR_JOB: "ADD_MEDIA_FOR_JOB",
TOGGLE_MEDIA_SELECTED: "TOGGLE_MEDIA_SELECTED",
GET_MEDIA_FOR_BILL: "GET_MEDIA_FOR_BILL",
SELECT_ALL_MEDIA_FOR_JOB: "SELECT_ALL_MEDIA_FOR_JOB",
DESELECT_ALL_MEDIA_FOR_JOB: "DESELECT_ALL_MEDIA_FOR_JOB",
};
export default MediaActionTypes;

View File

@@ -4,6 +4,7 @@ import storage from "redux-persist/lib/storage";
import { withReduxStateSync } from "redux-state-sync";
import applicationReducer from "./application/application.reducer";
import emailReducer from "./email/email.reducer";
import mediaReducer from "./media/media.reducer";
import messagingReducer from "./messaging/messaging.reducer";
import modalsReducer from "./modals/modals.reducer";
import techReducer from "./tech/tech.reducer";
@@ -29,6 +30,7 @@ const rootReducer = combineReducers({
modals: modalsReducer,
application: persistReducer(applicationPersistConfig, applicationReducer),
tech: techReducer,
media: mediaReducer,
});
export default withReduxStateSync(

View File

@@ -6,6 +6,7 @@ import { emailSagas } from "./email/email.sagas";
import { modalsSagas } from "./modals/modals.sagas";
import { applicationSagas } from "./application/application.sagas";
import { techSagas } from "./tech/tech.sagas";
import { mediaSagas } from "./media/media.sagas";
export default function* rootSaga() {
yield all([
@@ -15,5 +16,6 @@ export default function* rootSaga() {
call(modalsSagas),
call(applicationSagas),
call(techSagas),
call(mediaSagas),
]);
}

View File

@@ -97,7 +97,10 @@ const userReducer = (state = INITIAL_STATE, action) => {
};
case UserActionTypes.SET_SHOP_DETAILS:
return { ...state, bodyshop: action.payload };
return {
...state,
bodyshop: action.payload,
};
case UserActionTypes.SIGN_IN_FAILURE:
case UserActionTypes.SIGN_OUT_FAILURE:
case UserActionTypes.EMAIL_SIGN_UP_FAILURE:

View File

@@ -228,6 +228,7 @@
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -282,6 +283,9 @@
},
"last_name_first": "Display Owner Info as <Last>, <First>",
"lastnumberworkingdays": "Scoreboard - Last Number of Working Days",
"localmediaserverhttp": "Local Media Server - HTTP Path",
"localmediaservernetwork": "Local Media Server - Network Path",
"localmediatoken": "Local Media Server - Token",
"logo_img_footer_margin": "Footer Margin (px)",
"logo_img_header_margin": "Header Margin (px)",
"logo_img_path": "Shop Logo",
@@ -507,6 +511,7 @@
"timezone": "Timezone",
"tt_allow_post_to_invoiced": "Allow Time Tickets to be posted to Invoiced & Exported Jobs",
"use_fippa": "Use FIPPA for Names on Generated Documents?",
"uselocalmediaserver": "Use Local Media Server?",
"website": "Website",
"zip_post": "Zip/Postal Code"
},
@@ -544,6 +549,8 @@
"jobstatuses": "Job Statuses",
"laborrates": "Labor Rates",
"licensing": "Licensing",
"md_to_emails": "Preset To Emails",
"md_to_emails_emails": "Emails",
"messagingpresets": "Messaging Presets",
"notemplatesavailable": "No templates available to add.",
"notespresets": "Notes Presets",
@@ -822,6 +829,7 @@
"confirmdelete": "Are you sure you want to delete these documents. This CANNOT be undone.",
"doctype": "Document Type",
"newjobid": "Assign to Job",
"openinexplorer": "Open in Explorer",
"reassign_limitexceeded": "Reassigning all selected documents will exceed the job storage limit for your shop. ",
"reassign_limitexceeded_title": "Unable to reassign document(s)",
"storageexceeded": "You've exceeded your storage limit for this job. Please remove documents, or increase your storage plan.",
@@ -877,6 +885,7 @@
"base_rate": "Base Rate",
"cost_center": "Cost Center",
"employee_number": "Employee Number",
"external_id": "External Employee ID",
"first_name": "First Name",
"flat_rate": "Flat Rate (Disabled is Straight Time)",
"hire_date": "Hire Date",
@@ -912,7 +921,8 @@
"createdat": "Created At"
},
"labels": {
"attempts": "Export Attempts"
"attempts": "Export Attempts",
"priorsuccesfulexport": "This record has previously been exported successfully. Please make sure it has already been deleted in the target system."
}
},
"general": {
@@ -1856,6 +1866,7 @@
"updatedat": "Updated At"
},
"labels": {
"addtorelatedro": "Add to Related ROs",
"newnoteplaceholder": "Add a note...",
"notetoadd": "Note to Add"
},
@@ -1916,7 +1927,8 @@
},
"parts": {
"actions": {
"order": "Order Parts"
"order": "Order Parts",
"orderinhouse": "Order as In House"
}
},
"parts_orders": {
@@ -2169,6 +2181,9 @@
"ca_bc_etf_table": "ICBC ETF Table",
"exported_payroll": "Payroll Table"
},
"special": {
"attendance_detail_csv": "Attendance Table"
},
"subjects": {
"jobs": {
"parts_order": "Parts Order PO: {{ro_number}} - {{name}}"
@@ -2288,6 +2303,7 @@
"attendance_employee": "Employee Attendance",
"attendance_summary": "Attendance Summary (All Employees)",
"credits_not_received_date": "Credits not Received by Date",
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
"csi": "CSI Responses",
"estimates_written_converted": "Estimates Written/Converted",
"estimator_detail": "Jobs by Estimator (Detail)",

View File

@@ -228,6 +228,7 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -282,6 +283,9 @@
},
"last_name_first": "",
"lastnumberworkingdays": "",
"localmediaserverhttp": "",
"localmediaservernetwork": "",
"localmediatoken": "",
"logo_img_footer_margin": "",
"logo_img_header_margin": "",
"logo_img_path": "",
@@ -507,6 +511,7 @@
"timezone": "",
"tt_allow_post_to_invoiced": "",
"use_fippa": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": ""
},
@@ -544,6 +549,8 @@
"jobstatuses": "",
"laborrates": "",
"licensing": "",
"md_to_emails": "",
"md_to_emails_emails": "",
"messagingpresets": "",
"notemplatesavailable": "",
"notespresets": "",
@@ -822,6 +829,7 @@
"confirmdelete": "",
"doctype": "",
"newjobid": "",
"openinexplorer": "",
"reassign_limitexceeded": "",
"reassign_limitexceeded_title": "",
"storageexceeded": "",
@@ -877,6 +885,7 @@
"base_rate": "Tasa básica",
"cost_center": "Centro de costos",
"employee_number": "Numero de empleado",
"external_id": "",
"first_name": "Nombre de pila",
"flat_rate": "Tarifa plana (deshabilitado es tiempo recto)",
"hire_date": "Fecha de contratación",
@@ -912,7 +921,8 @@
"createdat": ""
},
"labels": {
"attempts": ""
"attempts": "",
"priorsuccesfulexport": ""
}
},
"general": {
@@ -1856,6 +1866,7 @@
"updatedat": "Actualizado en"
},
"labels": {
"addtorelatedro": "",
"newnoteplaceholder": "Agrega una nota...",
"notetoadd": ""
},
@@ -1916,7 +1927,8 @@
},
"parts": {
"actions": {
"order": "Pedido de piezas"
"order": "Pedido de piezas",
"orderinhouse": ""
}
},
"parts_orders": {
@@ -2169,6 +2181,9 @@
"ca_bc_etf_table": "",
"exported_payroll": ""
},
"special": {
"attendance_detail_csv": ""
},
"subjects": {
"jobs": {
"parts_order": ""
@@ -2288,6 +2303,7 @@
"attendance_employee": "",
"attendance_summary": "",
"credits_not_received_date": "",
"credits_not_received_date_vendorid": "",
"csi": "",
"estimates_written_converted": "",
"estimator_detail": "",

View File

@@ -228,6 +228,7 @@
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -282,6 +283,9 @@
},
"last_name_first": "",
"lastnumberworkingdays": "",
"localmediaserverhttp": "",
"localmediaservernetwork": "",
"localmediatoken": "",
"logo_img_footer_margin": "",
"logo_img_header_margin": "",
"logo_img_path": "",
@@ -507,6 +511,7 @@
"timezone": "",
"tt_allow_post_to_invoiced": "",
"use_fippa": "",
"uselocalmediaserver": "",
"website": "",
"zip_post": ""
},
@@ -544,6 +549,8 @@
"jobstatuses": "",
"laborrates": "",
"licensing": "",
"md_to_emails": "",
"md_to_emails_emails": "",
"messagingpresets": "",
"notemplatesavailable": "",
"notespresets": "",
@@ -822,6 +829,7 @@
"confirmdelete": "",
"doctype": "",
"newjobid": "",
"openinexplorer": "",
"reassign_limitexceeded": "",
"reassign_limitexceeded_title": "",
"storageexceeded": "",
@@ -877,6 +885,7 @@
"base_rate": "Taux de base",
"cost_center": "Centre de coûts",
"employee_number": "Numéro d'employé",
"external_id": "",
"first_name": "Prénom",
"flat_rate": "Taux fixe (désactivé est le temps normal)",
"hire_date": "Date d'embauche",
@@ -912,7 +921,8 @@
"createdat": ""
},
"labels": {
"attempts": ""
"attempts": "",
"priorsuccesfulexport": ""
}
},
"general": {
@@ -1856,6 +1866,7 @@
"updatedat": "Mis à jour à"
},
"labels": {
"addtorelatedro": "",
"newnoteplaceholder": "Ajouter une note...",
"notetoadd": ""
},
@@ -1916,7 +1927,8 @@
},
"parts": {
"actions": {
"order": "Commander des pièces"
"order": "Commander des pièces",
"orderinhouse": ""
}
},
"parts_orders": {
@@ -2169,6 +2181,9 @@
"ca_bc_etf_table": "",
"exported_payroll": ""
},
"special": {
"attendance_detail_csv": ""
},
"subjects": {
"jobs": {
"parts_order": ""
@@ -2288,6 +2303,7 @@
"attendance_employee": "",
"attendance_summary": "",
"credits_not_received_date": "",
"credits_not_received_date_vendorid": "",
"csi": "",
"estimates_written_converted": "",
"estimator_detail": "",

View File

@@ -18,13 +18,14 @@ export default async function RenderTemplate(
templateObject,
bodyshop,
renderAsHtml = false,
renderAsExcel = false
renderAsExcel = false,
renderAsText = false
) {
//Query assets that match the template name. Must be in format <<templateName>>.query
let { contextData, useShopSpecificTemplate } = await fetchContextData(
templateObject
);
console.log(templateObject.name);
const { ignoreCustomMargins } = Templates[templateObject.name];
let reportRequest = {
@@ -54,6 +55,7 @@ export default async function RenderTemplate(
}),
}),
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
...(renderAsText ? { recipe: "text" } : {}),
},
data: {
...contextData,
@@ -254,6 +256,8 @@ export const GenerateDocument = async (
} else if (sendType === "x") {
console.log("excel");
await RenderTemplate(template, bodyshop, false, true);
} else if (sendType === "text") {
await RenderTemplate(template, bodyshop, false, false, true);
} else {
await RenderTemplate(template, bodyshop);
}

View File

@@ -1510,6 +1510,22 @@ export const TemplateList = (type, context) => {
},
group: "sales",
},
credits_not_received_date_vendorid: {
title: i18n.t(
"reportcenter.templates.credits_not_received_date_vendorid"
),
subject: i18n.t(
"reportcenter.templates.credits_not_received_date_vendorid"
),
key: "credits_not_received_date_vendorid",
idtype: "vendor",
disabled: false,
rangeFilter: {
object: i18n.t("reportcenter.labels.objects.jobs"),
field: i18n.t("jobs.fields.date_open"),
},
group: "purchases",
},
}
: {}),
...(!type || type === "courtesycarcontract"
@@ -1668,6 +1684,13 @@ export const TemplateList = (type, context) => {
key: "exported_payroll",
disabled: false,
},
attendance_detail_csv: {
title: i18n.t("printcenter.special.attendance_detail_csv"),
description: "Est Detail",
subject: i18n.t("printcenter.special.attendance_detail_csv"),
key: "attendance_detail_csv",
disabled: false,
},
production_by_technician_one: {
title: i18n.t(
"reportcenter.templates.production_by_technician_one"

View File

@@ -0,0 +1,6 @@
import { store } from "../redux/store";
export function CreateExplorerLinkForJob({ jobid }) {
const bodyshop = store.getState().user.bodyshop;
return `imexmedia://${bodyshop.localmediaservernetwork}\\Jobs\\${jobid}`;
}

View File

@@ -9702,6 +9702,11 @@ normalize-url@^3.0.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
normalize-url@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-7.0.3.tgz#12e56889f7a54b2d5b09616f36c442a9063f61af"
integrity sha512-RiCOdwdPnzvwcBFJE4iI1ss3dMVRIrEzFpn8ftje6iBfzBInqlnRrNhxcLwBEKjPPXQKzm1Ptlxtaiv9wdcj5w==
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"

View File

@@ -779,6 +779,13 @@
table:
schema: public
name: timetickets
- name: transitions
using:
foreign_key_constraint_on:
column: bodyshopid
table:
schema: public
name: transitions
- name: vehicles
using:
foreign_key_constraint_on:
@@ -828,6 +835,9 @@
- jc_hourly_rates
- jobsizelimit
- last_name_first
- localmediaserverhttp
- localmediaservernetwork
- localmediatoken
- logo_img_path
- md_categories
- md_ccc_rates
@@ -851,6 +861,7 @@
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- md_to_emails
- messagingservicesid
- pbs_configuration
- pbs_serialnumber
@@ -877,6 +888,7 @@
- tt_allow_post_to_invoiced
- updated_at
- use_fippa
- uselocalmediaserver
- website
- workingdays
- zip_post
@@ -914,6 +926,9 @@
- intakechecklist
- jc_hourly_rates
- last_name_first
- localmediaserverhttp
- localmediaservernetwork
- localmediatoken
- logo_img_path
- md_categories
- md_ccc_rates
@@ -937,6 +952,7 @@
- md_referral_sources
- md_responsibility_centers
- md_ro_statuses
- md_to_emails
- pbs_configuration
- phone
- prodtargethrs
@@ -956,6 +972,7 @@
- tt_allow_post_to_invoiced
- updated_at
- use_fippa
- uselocalmediaserver
- website
- workingdays
- zip_post
@@ -1946,6 +1963,7 @@
- active
- created_at
- employee_number
- external_id
- first_name
- flat_rate
- hire_date
@@ -1964,6 +1982,7 @@
- active
- created_at
- employee_number
- external_id
- first_name
- flat_rate
- hire_date
@@ -1992,6 +2011,7 @@
- active
- created_at
- employee_number
- external_id
- first_name
- flat_rate
- hire_date
@@ -2596,6 +2616,13 @@
insertion_order: null
column_mapping:
id: jobid
- name: mixdata
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: mixdata
- name: notes
using:
foreign_key_constraint_on:
@@ -2645,6 +2672,13 @@
table:
schema: public
name: timetickets
- name: transitions
using:
foreign_key_constraint_on:
column: jobid
table:
schema: public
name: transitions
insert_permissions:
- role: user
permission:
@@ -3590,6 +3624,84 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: mixdata
object_relationships:
- name: job
using:
foreign_key_constraint_on: jobid
insert_permissions:
- role: user
permission:
check:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
columns:
- mixdata
- totalliquidcost
- totalsundrycost
- company
- version
- created_at
- updated_at
- id
- jobid
backend_only: false
select_permissions:
- role: user
permission:
columns:
- mixdata
- totalliquidcost
- totalsundrycost
- company
- version
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- mixdata
- totalliquidcost
- totalsundrycost
- company
- version
- created_at
- updated_at
- id
- jobid
filter:
job:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
check: null
- table:
schema: public
name: notes
@@ -4597,6 +4709,93 @@
_eq: X-Hasura-User-Id
- active:
_eq: true
- table:
schema: public
name: transitions
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: job
using:
foreign_key_constraint_on: jobid
insert_permissions:
- role: user
permission:
check:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
columns:
- bodyshopid
- created_at
- duration
- end
- id
- jobid
- next_value
- prev_value
- start
- type
- updated_at
- value
backend_only: false
select_permissions:
- role: user
permission:
columns:
- duration
- next_value
- prev_value
- type
- value
- created_at
- end
- start
- updated_at
- bodyshopid
- id
- jobid
filter:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
update_permissions:
- role: user
permission:
columns:
- duration
- next_value
- prev_value
- type
- value
- created_at
- end
- start
- updated_at
- bodyshopid
- id
- jobid
filter:
bodyshop:
associations:
_and:
- active:
_eq: true
- user:
authid:
_eq: X-Hasura-User-Id
check: {}
- table:
schema: public
name: users

View File

@@ -0,0 +1 @@
DROP TABLE "public"."transitions";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."transitions" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshoipid" uuid NOT NULL, "start" timestamptz NOT NULL, "end" timestamptz, "duration" numeric DEFAULT 0, "prev_value" text, "value" text, "next_value" Text, "jobid" uuid, "type" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshoipid") REFERENCES "public"."bodyshops"("id") ON UPDATE cascade ON DELETE cascade, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_transitions_updated_at"
BEFORE UPDATE ON "public"."transitions"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_transitions_updated_at" ON "public"."transitions"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1 @@
alter table "public"."transitions" rename column "bodyshopid" to "bodyshoipid";

View File

@@ -0,0 +1 @@
alter table "public"."transitions" rename column "bodyshoipid" to "bodyshopid";

View File

@@ -0,0 +1 @@
DROP TABLE "public"."mixdata";

View File

@@ -0,0 +1,18 @@
CREATE TABLE "public"."mixdata" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "jobid" uuid NOT NULL, "company" text NOT NULL, "version" text NOT NULL, "totalliquidcost" numeric NOT NULL, "totalsundrycost" numeric NOT NULL, "mixdata" jsonb, PRIMARY KEY ("id") , FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE cascade ON DELETE cascade);
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
RETURNS TRIGGER AS $$
DECLARE
_new record;
BEGIN
_new := NEW;
_new."updated_at" = NOW();
RETURN _new;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "set_public_mixdata_updated_at"
BEFORE UPDATE ON "public"."mixdata"
FOR EACH ROW
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_mixdata_updated_at" ON "public"."mixdata"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE EXTENSION IF NOT EXISTS pgcrypto;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "md_to_emails" jsonb
-- null default jsonb_build_array();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "md_to_emails" jsonb
null default jsonb_build_array();

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."employees" add column "external_id" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."employees" add column "external_id" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "uselocalmediaserver" boolean
-- not null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "uselocalmediaserver" boolean
not null default 'false';

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "localmediaserverhttp" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "localmediaserverhttp" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "localmediaservernetwork" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "localmediaservernetwork" text
null;

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "localmediatoken" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "localmediatoken" text
null;

File diff suppressed because one or more lines are too long

9612
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
"version": "0.0.1",
"license": "UNLICENSED",
"engines": {
"node": "12.22.6",
"npm": "7.17.0"
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"scripts": {
"setup": "yarn && cd client && yarn",
@@ -39,6 +39,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.3",
"moment-timezone": "^0.5.34",
"multer": "^1.4.4",
"node-mailjet": "^3.3.10",
"node-quickbooks": "^2.0.39",
"nodemailer": "^6.7.3",
@@ -50,6 +51,7 @@
"stripe": "^8.217.0",
"twilio": "^3.76.1",
"uuid": "^8.3.2",
"xml2js": "^0.4.23",
"xmlbuilder2": "^3.0.2"
},
"devDependencies": {

View File

@@ -7,7 +7,8 @@ const twilio = require("twilio");
const logger = require("./server/utils/logger");
var fb = require("./server/firebase/firebase-handler");
var cookieParser = require("cookie-parser");
const multer = require("multer");
const upload = multer();
//var enforce = require("express-sslify");
require("dotenv").config({
@@ -123,6 +124,11 @@ app.post("/sms/markConversationRead", smsStatus.markConversationRead);
var job = require("./server/job/job");
app.post("/job/totals", fb.validateFirebaseIdToken, job.totals);
app.post(
"/job/statustransition",
fb.validateFirebaseIdToken,
job.statustransition
);
app.post("/job/totalsssu", fb.validateFirebaseIdToken, job.totalsSsu);
app.post("/job/costing", fb.validateFirebaseIdToken, job.costing);
app.post("/job/costingmulti", fb.validateFirebaseIdToken, job.costingmulti);
@@ -181,6 +187,15 @@ app.post("/data/arms", data.arms);
var taskHandler = require("./server/tasks/tasks");
app.post("/taskHandler", taskHandler.taskHandler);
var mixdataUpload = require("./server/mixdata/mixdata");
app.post(
"/mixdata/upload",
fb.validateFirebaseIdToken,
upload.any(),
mixdataUpload.mixdataUpload
);
var ioevent = require("./server/ioevent/ioevent");
app.post("/ioevent", ioevent.default);
app.post("/newlog", (req, res) => {

View File

@@ -198,6 +198,8 @@ async function QueryJobData(socket, jobid) {
async function QueryVehicleFromDms(socket) {
try {
if (!socket.JobData.v_vin) return null;
const { data: VehicleGetResponse, request } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{

View File

@@ -637,6 +637,13 @@ exports.default = function ({
});
}
if (!qbo && InvoiceLineAdd.length === 0) {
//Handle the scenario where there is a $0 sale invoice.
InvoiceLineAdd.push({
Desc: "No estimate lines.",
});
}
return InvoiceLineAdd;
};

View File

@@ -71,7 +71,7 @@ exports.default = async (req, res) => {
exports.refresh = async (oauthClient, req) => {
try {
logger.log("qbo-token-refresh", "DEBUG", req.user.email, null, null);
// logger.log("qbo-token-refresh", "DEBUG", req.user.email, null, null);
const authResponse = await oauthClient.refresh();
await client.request(queries.SET_QBO_AUTH, {
email: req.user.email,
@@ -85,7 +85,7 @@ exports.refresh = async (oauthClient, req) => {
};
exports.setNewRefreshToken = async (email, apiResponse) => {
logger.log("qbo-token-updated", "DEBUG", email, null, null);
//logger.log("qbo-token-updated", "DEBUG", email, null, null);
await client.request(queries.SET_QBO_AUTH, {
email,

View File

@@ -45,7 +45,7 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { bills: billsToQuery } = req.body;
const { bills: billsToQuery, elgen } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
@@ -59,8 +59,9 @@ exports.default = async (req, res) => {
bills: billsToQuery,
});
const { bills } = result;
const { bills, bodyshops } = result;
const ret = [];
const bodyshop = bodyshops[0];
for (const bill of bills) {
try {
@@ -86,9 +87,31 @@ exports.default = async (req, res) => {
qbo_realmId,
req,
bill,
vendorRecord
vendorRecord,
bodyshop
);
// //No error. Mark the job exported & insert export log.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QBO_MARK_BILL_EXPORTED, {
billId: bill.id,
bill: {
exported: true,
exported_at: moment().tz(bodyshop.timezone),
},
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: true,
useremail: req.user.email,
},
],
});
}
ret.push({ billid: bill.id, success: true });
} catch (error) {
ret.push({
@@ -98,6 +121,26 @@ exports.default = async (req, res) => {
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
});
//Add the export log error.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
billid: bill.id,
successful: false,
message: JSON.stringify([
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
]),
useremail: req.user.email,
},
],
});
}
}
}
@@ -167,7 +210,14 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
}
}
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
async function InsertBill(
oauthClient,
qbo_realmId,
req,
bill,
vendor,
bodyshop
) {
const { accounts, taxCodes, classes } = await QueryMetaData(
oauthClient,
qbo_realmId,
@@ -179,20 +229,20 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
il,
accounts,
bill.job.class,
bill.job.bodyshop.md_responsibility_centers.sales_tax_codes,
bodyshop.md_responsibility_centers.sales_tax_codes,
classes,
taxCodes,
bill.job.bodyshop.md_responsibility_centers.costs
bodyshop.md_responsibility_centers.costs
)
);
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
bill.job.bodyshop.accountingconfig &&
bill.job.bodyshop.accountingconfig.qbo &&
bill.job.bodyshop.accountingconfig.qbo_usa &&
bill.job.bodyshop.region_config.includes("CA_")
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_")
) {
lines.push({
DetailType: "AccountBasedExpenseLineDetail",
@@ -204,8 +254,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
AccountRef: {
value:
accounts[
bill.job.bodyshop.md_responsibility_centers.taxes.federal
.accountdesc
bodyshop.md_responsibility_centers.taxes.federal.accountdesc
],
},
},
@@ -239,7 +288,18 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor) {
}),
DocNumber: bill.invoice_number,
//...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}),
...(!(
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&
bodyshop.region_config.includes("CA_")
)
? { GlobalTaxCalculation: "TaxExcluded" }
: {}),
...(bodyshop.accountingconfig.qbo_departmentid &&
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid },
}),
PrivateNote: `RO ${bill.job.ro_number || ""}`,
Line: lines,
};

View File

@@ -52,7 +52,7 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { payments: paymentsToQuery } = req.body;
const { payments: paymentsToQuery, elgen } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
@@ -155,6 +155,27 @@ exports.default = async (req, res) => {
bodyshop
);
}
// //No error. Mark the payment exported & insert export log.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QBO_MARK_PAYMENT_EXPORTED, {
paymentId: payment.id,
payment: {
exportedat: moment().tz(bodyshop.timezone),
},
logs: [
{
bodyshopid: bodyshop.id,
paymentid: payment.id,
successful: true,
useremail: req.user.email,
},
],
});
}
ret.push({ paymentid: payment.id, success: true });
} catch (error) {
logger.log("qbo-payment-create-error", "ERROR", req.user.email, {
@@ -162,6 +183,25 @@ exports.default = async (req, res) => {
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
});
//Add the export log error.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
paymentid: payment.id,
successful: false,
message: JSON.stringify([
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
]),
useremail: req.user.email,
},
],
});
}
ret.push({
paymentid: payment.id,

View File

@@ -46,7 +46,7 @@ exports.default = async (req, res) => {
await refreshOauthToken(oauthClient, req);
const BearerToken = req.headers.authorization;
const { jobIds } = req.body;
const { jobIds, elgen } = req.body;
//Query Job Info
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
@@ -139,6 +139,28 @@ exports.default = async (req, res) => {
bodyshop,
jobTier
);
// //No error. Mark the job exported & insert export log.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.QBO_MARK_JOB_EXPORTED, {
jobId: job.id,
job: {
status:
bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: moment().tz(bodyshop.timezone),
},
logs: [
{
bodyshopid: bodyshop.id,
jobid: job.id,
successful: true,
useremail: req.user.email,
},
],
});
}
}
ret.push({ jobid: job.id, success: true });
} catch (error) {
@@ -149,6 +171,25 @@ exports.default = async (req, res) => {
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
});
//Add the export log error.
if (elgen) {
const result = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_EXPORT_LOG, {
logs: [
{
bodyshopid: bodyshop.id,
jobid: job.id,
successful: false,
message: JSON.stringify([
(error && error.authResponse && error.authResponse.body) ||
(error && error.message),
]),
useremail: req.user.email,
},
],
});
}
}
}
@@ -198,6 +239,7 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
const Customer = {
DisplayName: job.ins_co_nm,
BillWithParent: true,
BillAddr: {
City: job.ownr_city,
Line1: insCo.street1,
@@ -261,6 +303,7 @@ async function InsertOwner(
const ownerName = generateOwnerTier(job, true, null);
const Customer = {
DisplayName: ownerName,
BillWithParent: true,
BillAddr: {
City: job.ownr_city,
Line1: job.ownr_addr1,
@@ -321,6 +364,7 @@ exports.QueryJob = QueryJob;
async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
const Customer = {
DisplayName: job.ro_number,
BillWithParent: true,
BillAddr: {
City: job.ownr_city,
Line1: job.ownr_addr1,
@@ -459,7 +503,38 @@ async function InsertInvoice(
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid },
}),
CustomField: [
...(bodyshop.accountingconfig.ReceivableCustomField1
? [
{
DefinitionId: "1",
StringValue:
job[bodyshop.accountingconfig.ReceivableCustomField1],
Type: "StringType",
},
]
: []),
...(bodyshop.accountingconfig.ReceivableCustomField2
? [
{
DefinitionId: "2",
StringValue:
job[bodyshop.accountingconfig.ReceivableCustomField2],
Type: "StringType",
},
]
: []),
...(bodyshop.accountingconfig.ReceivableCustomField3
? [
{
DefinitionId: "3",
StringValue:
job[bodyshop.accountingconfig.ReceivableCustomField3],
Type: "StringType",
},
]
: []),
],
...(bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
bodyshop.accountingconfig.qbo_usa &&

View File

@@ -37,14 +37,15 @@ exports.default = async (req, res) => {
.request(queries.QUERY_BILLS_FOR_PAYABLES_EXPORT, {
bills: billsToQuery,
});
const { bills } = result;
const { bills, bodyshops } = result;
const bodyshop = bodyshops[0];
const QbXmlToExecute = [];
bills.map((i) => {
QbXmlToExecute.push({
id: i.id,
okStatusCodes: ["0"],
qbxml: generateBill(i),
qbxml: generateBill(i, bodyshop),
});
});
@@ -62,7 +63,7 @@ exports.default = async (req, res) => {
}
};
const generateBill = (bill) => {
const generateBill = (bill, bodyshop) => {
const billQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
@@ -87,7 +88,7 @@ const generateBill = (bill) => {
ExpenseLineAdd: bill.billlines.map((il) =>
generateBillLine(
il,
bill.job.bodyshop.md_responsibility_centers,
bodyshop.md_responsibility_centers,
bill.job.class
)
),

View File

@@ -139,6 +139,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
jobs(where: {id: {_in: $ids}}) {
id
job_totals
ded_amt
date_invoiced
ro_number
clm_total
@@ -217,6 +218,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
accountingconfig
md_ins_cos
timezone
md_ro_statuses
}
}
`;
@@ -408,6 +410,13 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
exports.QUERY_BILLS_FOR_PAYABLES_EXPORT = `
query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
bodyshops(where: {associations: {active: {_eq: true}}}) {
id
md_responsibility_centers
timezone
region_config
accountingconfig
}
bills(where: {id: {_in: $bills}}) {
id
date
@@ -424,12 +433,6 @@ query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
ownr_ln
ownr_co_nm
class
bodyshop{
md_responsibility_centers
timezone
region_config
accountingconfig
}
}
billlines{
id
@@ -457,6 +460,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
md_responsibility_centers
accountingconfig
timezone
md_ins_cos
}
payments(where: {id: {_in: $payments}}) {
id
@@ -484,6 +488,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
bodyshop{
accountingconfig
md_responsibility_centers
md_ins_cos
}
}
transactionid
@@ -1482,6 +1487,7 @@ mutation MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $log: exportlog
}
}
`;
exports.INSERT_EXPORT_LOG = `
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
insert_exportlog_one(object: $log) {
@@ -1489,3 +1495,97 @@ mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
}
}
`;
exports.QUERY_EXISTING_TRANSITION = `
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
insert_exportlog_one(object: $log) {
id
}
}
`;
exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $existingTransition: transitions_set_input!){
update_transitions(where:{jobid:{_eq:$jobid}, end:{_is_null:true
}}, _set:$existingTransition){
affected_rows
returning{
id
start
end
prev_value
next_value
value
}
}
}`;
exports.INSERT_NEW_TRANSITION = `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, $oldTransitionId: uuid, $duration: numeric) {
insert_transitions_one(object: $newTransition) {
id
}
update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}
}
`;
exports.QUERY_JOB_ID_MIXDATA = `query QUERY_JOB_ID_MIXDATA($roNumbers: [String!]!) {
jobs(where: {ro_number: {_in: $roNumbers}}) {
id
ro_number
mixdata {
id
}
}
}
`;
exports.QBO_MARK_JOB_EXPORTED = `
mutation QBO_MARK_JOB_EXPORTED($jobId: uuid!, $job: jobs_set_input!, $logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
update_jobs(where: {id: {_eq: $jobId}}, _set: $job) {
returning {
id
}
}
}
`;
exports.QBO_MARK_BILL_EXPORTED = `
mutation QBO_MARK_BILL_EXPORTED($billId: uuid!, $bill: bills_set_input!, $logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
update_bills(where: { id: { _eq: $billId } }, _set: $bill) {
returning {
id
}
}
}
`;
exports.QBO_MARK_PAYMENT_EXPORTED = `
mutation QBO_MARK_PAYMENT_EXPORTED($paymentId: uuid!, $payment: payments_set_input!, $logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
update_payments(where: {id: {_eq: $paymentId}}, _set: $payment) {
returning {
id
}
}
}`;
exports.INSERT_EXPORT_LOG = `
mutation INSERT_EXPORT_LOG($logs: [exportlog_insert_input!]!) {
insert_exportlog(objects: $logs) {
affected_rows
}
}
`;

View File

@@ -0,0 +1,84 @@
const Dinero = require("dinero.js");
const queries = require("../graphql-client/queries");
//const client = require("../graphql-client/graphql-client").client;
const _ = require("lodash");
const GraphQLClient = require("graphql-request").GraphQLClient;
const logger = require("../utils/logger");
// Dinero.defaultCurrency = "USD";
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
async function StatusTransition(req, res) {
const { jobid, value, bodyshopid } = req.body;
const BearerToken = req.headers.authorization;
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
headers: {
Authorization: BearerToken,
},
});
try {
const { update_transitions } = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.UPDATE_OLD_TRANSITION, {
jobid: jobid,
existingTransition: {
end: new Date(),
next_value: value,
//duration
},
});
let duration =
update_transitions.affected_rows === 0
? 0
: new Date(update_transitions.returning[0].end) -
new Date(update_transitions.returning[0].start);
const resp2 = await client
.setHeaders({ Authorization: BearerToken })
.request(queries.INSERT_NEW_TRANSITION, {
oldTransitionId:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].id,
duration,
newTransition: {
bodyshopid: bodyshopid,
jobid: jobid,
start:
update_transitions.affected_rows === 0
? new Date()
: update_transitions.returning[0].end,
prev_value:
update_transitions.affected_rows === 0
? null
: update_transitions.returning[0].value,
value: value,
type: "status",
},
});
//Check to see if there is an existing status transition record.
//Query using Job ID, start is not null, end is null.
//If there is no existing record, this is the start of the transition life cycle.
// Create the initial transition record.
//If there is a current status transition record, update it with the end date, duration, and next value.
res.sendStatus(200); //.json(ret);
} catch (error) {
logger.log("job-costing-error", "ERROR", req.user.email, jobid, {
message: error.message,
stack: error.stack,
});
res.status(400).send(JSON.stringify(error));
}
}
exports.statustransition = StatusTransition;

View File

@@ -2,3 +2,4 @@ exports.totals = require("./job-totals").default;
exports.totalsSsu = require("./job-totals").totalsSsu;
exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition;

View File

@@ -0,0 +1,206 @@
{
"PPG": {
"Header": {
"Protocol": {
"Message": "MixDataInterface",
"Name": "PPG",
"Version": "1.3.0"
},
"Transaction": {
"TransactionID": "3F2504E0-4F89-11D3-9A0C-0305E82C3301",
"TransactionDate": "2006-06-06T15:00:00"
},
"ShopInfo": {
"ShopID": "SomeShopID",
"ShopName": "Some Body Shop"
}
},
"DataExportInterface": {
"ROData": {
"ROCount": "2",
"RepairOrders": {
"RO": [
{
"ROCounter": "1",
"RONumber": "27187",
"Notes": "This is a painter note",
"Undercoat": "False",
"Clearcoat": "False",
"Basecoat": "True",
"TotalLiquidCost": "133.26",
"TotalSundryCost": "47.12",
"MixCount": "2",
"Mixes": {
"Mix": [
{
"MixCounter": "1",
"MixRONumber": "27187",
"MixedDate": "2008-09-17T00:00:00",
"MixedBy": "Tony Blair",
"MixedByEmployeeID": "5>",
"PPGBrandCode": "ABC",
"MixCost": "83.49",
"FormulaType": "Standard",
"ComponentCount": "5",
"Components": {
"Component": [
{
"ComponentCounter": "1",
"ComponentRONumber": "27187",
"ComponentCode": "P425-900",
"ComponentDescription": "F3550 CLEAR",
"ComponentCost": "44.84",
"ComponentWeightApplied": "521.5347",
"ComponentWeightTarget": "525.4023",
"ComponentDensity": "1.311"
},
{
"ComponentCounter": "2",
"ComponentRONumber": "27187",
"ComponentCode": "P425-948",
"ComponentDescription": "H/S BLACK",
"ComponentCost": "13.77",
"ComponentWeightApplied": "118.5779",
"ComponentWeightTarget": "118.5832",
"ComponentDensity": "0.971"
},
{
"ComponentCounter": "3",
"ComponentRONumber": "27187",
"ComponentCode": "P425-921",
"ComponentDescription": "H/S CLARET",
"ComponentCost": "0.24",
"ComponentWeightApplied": "2.082",
"ComponentWeightTarget": "2.1022",
"ComponentDensity": "0.976"
},
{
"ComponentCounter": "4",
"ComponentRONumber": "27187",
"ComponentCode": "P425-937",
"ComponentDescription": "PALE YELLOW",
"ComponentCost": "2.85",
"ComponentWeightApplied": "20.4412",
"ComponentWeightTarget": "20.4888",
"ComponentDensity": "1.136"
},
{
"ComponentCounter": "5",
"ComponentRONumber": "27187",
"ComponentCode": "P425-948",
"ComponentDescription": "Matting Agent",
"ComponentCost": "21.79",
"ComponentWeightApplied": "414.8808",
"ComponentWeightTarget": "414.8798",
"ComponentDensity": "0.987"
}
]
}
},
{
"MixCounter": "2",
"MixRONumber": "27187",
"MixedDate": "2008-09-17T00:00:00",
"MixedBy": "Bill Clinton",
"MixedByEmployeeID": "42>",
"PPGBrandCode": "DEF",
"MixCost": "49.76",
"FormulaType": "RFU",
"ComponentCount": "2",
"Components": {
"Component": [
{
"ComponentCounter": "1",
"ComponentRONumber": "27187",
"ComponentCode": "P850-1401",
"ComponentDescription": "Fade-Out Thinner",
"ComponentCost": "17.34",
"ComponentWeightApplied": "837.1649",
"ComponentWeightTarget": "838.2232",
"ComponentDensity": "0.8716"
},
{
"ComponentCounter": "2",
"ComponentRONumber": "27187",
"ComponentCode": "P190-478",
"ComponentDescription": "Ultraclear",
"ComponentCost": "32.42",
"ComponentWeightApplied": "862.5329",
"ComponentWeightTarget": "870.3238",
"ComponentDensity": "0.898"
}
]
}
}
]
},
"SundryCount": "1",
"Sundries": {
"Sundry": {
"SundryCounter": "1",
"SundryRONumber": "27187",
"SundryAddedDate": "2008-09-17T00:00:00",
"SundryAddedBy": "Tony Blair",
"SundryCode": "Sundry1",
"SundryDescription": "Bumper Prep Kit",
"SundryCost": "23.56",
"SundryQuantity": "2",
"SundryQuantityUOM": "Each"
}
}
},
{
"ROCounter": "2",
"RONumber": "27320",
"Notes": "This is a painter note too",
"Undercoat": "True",
"Clearcoat": "False",
"Basecoat": "False",
"TotalLiquidCost": "9.59",
"TotalSundryCost": "0.0",
"MixCount": "1",
"Mixes": {
"Mix": {
"MixCounter": "1",
"MixRONumber": "27320",
"MixedDate": "2008-12-03T00:00:00",
"MixedBy": "Jerry Primer",
"MixedByEmployeeID": "87>",
"PPGBrandCode": "GHI",
"MixCost": "9.59",
"FormulaType": "Standard",
"ComponentCount": "2",
"Components": {
"Component": [
{
"ComponentCounter": "1",
"ComponentRONumber": "27320",
"ComponentCode": "P565-957",
"ComponentDescription": "Long Life Etch",
"ComponentCost": "5.31",
"ComponentWeightApplied": "117.8754",
"ComponentWeightTarget": "116.0129",
"ComponentDensity": "0.9808"
},
{
"ComponentCounter": "2",
"ComponentRONumber": "27320",
"ComponentCode": "P275-61",
"ComponentDescription": "Acitvator For Long Life",
"ComponentCost": "4.28",
"ComponentWeightApplied": "103.4325",
"ComponentWeightTarget": "101.3950",
"ComponentDensity": "0.8571"
}
]
}
}
},
"SundryCount": "0"
}
]
}
}
}
}
}

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