prev.billlines[index] &&
- prev.billlines[index].deductfromlabor !==
+ prev.billlines[index].deductedfromlbr !==
cur.billlines[index] &&
- cur.billlines[index].deductfromlabor
+ cur.billlines[index].deductedfromlbr
}
>
{() => {
@@ -268,7 +268,7 @@ export function BillEnterModalLinesComponent({
getFieldValue([
"billlines",
field.name,
- "deductfromlabor",
+ "deductedfromlbr",
])
)
return (
diff --git a/client/src/components/job-costing-modal/job-costing-modal.component.jsx b/client/src/components/job-costing-modal/job-costing-modal.component.jsx
index a811e69ec..ab99d2512 100644
--- a/client/src/components/job-costing-modal/job-costing-modal.component.jsx
+++ b/client/src/components/job-costing-modal/job-costing-modal.component.jsx
@@ -176,7 +176,7 @@ export function JobCostingModalComponent({ bodyshop, job }) {
).toFixed(2);
if (isNaN(summaryData.gppercent)) summaryData.gppercentFormatted = 0;
else if (!isFinite(summaryData.gppercent))
- summaryData.gppercentFormatted = "-∞";
+ summaryData.gppercentFormatted = "- ∞";
else {
summaryData.gppercentFormatted = summaryData.gppercent;
}
diff --git a/client/src/components/job-reconciliation-modal/job-reconciliation.modal.container.jsx b/client/src/components/job-reconciliation-modal/job-reconciliation.modal.container.jsx
index cd45c8722..3850cf359 100644
--- a/client/src/components/job-reconciliation-modal/job-reconciliation.modal.container.jsx
+++ b/client/src/components/job-reconciliation-modal/job-reconciliation.modal.container.jsx
@@ -1,11 +1,15 @@
+import { useQuery } from "@apollo/client";
import { Modal } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
+import { GET_JOB_RECONCILIATION_BY_PK } from "../../graphql/jobs.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectReconciliation } from "../../redux/modals/modals.selectors";
import JobReconciliationModalComponent from "./job-reconciliation-modal.component";
+import LoadingSpinner from "../loading-spinner/loading-spinner.component";
+import AlertComponent from "../alert/alert.component";
const mapStateToProps = createStructuredSelector({
reconciliationModal: selectReconciliation,
@@ -20,7 +24,12 @@ function JobReconciliationModalContainer({
}) {
const { t } = useTranslation();
const { context, visible } = reconciliationModal;
- const { job, bills } = context;
+ const { job } = context;
+
+ const { loading, error, data } = useQuery(GET_JOB_RECONCILIATION_BY_PK, {
+ variables: { id: job && job.id },
+ skip: !(job && job.id) || !visible,
+ });
const handleCancel = () => {
toggleModalVisible();
@@ -37,7 +46,15 @@ function JobReconciliationModalContainer({
cancelButtonProps={{ display: "none" }}
destroyOnClose
>
-
+
+ {error && }
+ {data && (
+
+ )}
+
);
}
diff --git a/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.component.jsx b/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.component.jsx
index 089dfe1d4..4c35fdd94 100644
--- a/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.component.jsx
+++ b/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.component.jsx
@@ -3,6 +3,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { alphaSort } from "../../utils/sorters";
+import "./job-reconciliation-parts-table.styles.scss";
export default function JobReconcilitionPartsTable({
jobLineState,
@@ -114,7 +115,11 @@ export default function JobReconcilitionPartsTable({
onChange: handleOnRowClick,
selectedRowKeys: selectedLines,
}}
+ rowClassName={(record) => record.removed && "text-strikethrough"}
/>
+
+ {t("jobs.labels.reconciliation.removedpartsstrikethrough")}
+
);
}
diff --git a/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.styles.scss b/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.styles.scss
new file mode 100644
index 000000000..fffdb8790
--- /dev/null
+++ b/client/src/components/job-reconciliation-parts-table/job-reconciliation-parts-table.styles.scss
@@ -0,0 +1,3 @@
+.text-strikethrough {
+ text-decoration: line-through;
+}
diff --git a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx
index 4f3474750..11330c5b9 100644
--- a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx
+++ b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.component.jsx
@@ -39,7 +39,9 @@ export default function JobReconciliationTotals({
return acc.add(
Dinero({
amount: Math.round((val.actual_price || 0) * 100),
- }).multiply(val.quantity || 1)
+ })
+ .multiply(val.quantity || 1)
+ .multiply(val.bill.is_credit_memo ? -1 : 1)
);
}, Dinero()),
};
@@ -97,6 +99,7 @@ export default function JobReconciliationTotals({
onClick={() => {
jobLineState[1]([]);
billLineState[1]([]);
+ setErrors([]);
}}
>
{t("jobs.labels.reconciliation.clear")}
diff --git a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js
index e679b1102..cb4b9fc82 100644
--- a/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js
+++ b/client/src/components/job-reconciliation-totals/job-reconciliation-totals.utility.js
@@ -23,11 +23,6 @@ export const reconcileByAssocLine = (
setErrors((errors) => [
...errors,
..._.uniqBy(duplicatedJobLinesbyInvoiceId).map((dupedId) => {
- console.log(
- "dupedId",
- dupedId,
- billLines.find((b) => b.id === dupedId)
- );
return i18next.t("jobs.labels.reconciliation.multiplebilllines", {
line_desc: jobLines.find((j) => j.id === dupedId)?.line_desc,
});
diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx
index b01e0b352..b2ab847ac 100644
--- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx
+++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx
@@ -14,6 +14,11 @@ import { TemplateList } from "../../utils/TemplateConstants";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { Link } from "react-router-dom";
+import parsePhoneNumber from "libphonenumber-js";
+import {
+ openChatByPhone,
+ setMessage,
+} from "../../redux/messaging/messaging.actions";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser'
@@ -21,12 +26,16 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
+ openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
+ setMessage: (text) => dispatch(setMessage(text)),
});
export function JobsDetailHeaderCsi({
setEmailOptions,
bodyshop,
job,
+ openChatByPhone,
+ setMessage,
...props
}) {
const { t } = useTranslation();
@@ -36,52 +45,90 @@ export function JobsDetailHeaderCsi({
const handleCreateCsi = async (e) => {
logImEXEvent("job_create_csi");
- const questionSetResult = await client.query({
- query: GET_CURRENT_QUESTIONSET_ID,
- });
+ //Is tehre already a CSI?
+ if (job.csi_invites.length === 0) {
+ const questionSetResult = await client.query({
+ query: GET_CURRENT_QUESTIONSET_ID,
+ });
- if (questionSetResult.data.csiquestions.length > 0) {
- const result = await insertCsi({
- variables: {
- csiInput: {
- jobid: job.id,
- bodyshopid: bodyshop.id,
- questionset: questionSetResult.data.csiquestions[0].id,
- relateddata: {
- job: {
- id: job.id,
- ownr_fn: job.ownr_fn,
- ro_number: job.ro_number,
- v_model_yr: job.v_model_yr,
- v_make_desc: job.v_make_desc,
- v_model_desc: job.v_model_desc,
- },
- bodyshop: {
- city: bodyshop.city,
- email: bodyshop.email,
- state: bodyshop.state,
- country: bodyshop.country,
- address1: bodyshop.address1,
- address2: bodyshop.address2,
- shopname: bodyshop.shopname,
- zip_post: bodyshop.zip_post,
- logo_img_path: bodyshop.logo_img_path,
+ if (questionSetResult.data.csiquestions.length > 0) {
+ const result = await insertCsi({
+ variables: {
+ csiInput: {
+ jobid: job.id,
+ bodyshopid: bodyshop.id,
+ questionset: questionSetResult.data.csiquestions[0].id,
+ relateddata: {
+ job: {
+ id: job.id,
+ ownr_fn: job.ownr_fn,
+ ro_number: job.ro_number,
+ v_model_yr: job.v_model_yr,
+ v_make_desc: job.v_make_desc,
+ v_model_desc: job.v_model_desc,
+ },
+ bodyshop: {
+ city: bodyshop.city,
+ email: bodyshop.email,
+ state: bodyshop.state,
+ country: bodyshop.country,
+ address1: bodyshop.address1,
+ address2: bodyshop.address2,
+ shopname: bodyshop.shopname,
+ zip_post: bodyshop.zip_post,
+ logo_img_path: bodyshop.logo_img_path,
+ },
},
},
},
- },
- });
+ });
- if (!!!result.errors) {
- notification["success"]({ message: t("csi.successes.created") });
+ if (!!!result.errors) {
+ notification["success"]({ message: t("csi.successes.created") });
+ } else {
+ notification["error"]({
+ message: t("csi.errors.creating", {
+ message: JSON.stringify(result.errors),
+ }),
+ });
+ return;
+ }
+ if (e.key === "email")
+ setEmailOptions({
+ messageOptions: {
+ to: [job.ownr_ea],
+ replyTo: bodyshop.email,
+ },
+ template: {
+ name: TemplateList("job").csi_invitation.key,
+ variables: {
+ id: result.data.insert_csi.returning[0].id,
+ },
+ },
+ });
+
+ if (e.key === "text") {
+ const p = parsePhoneNumber(job.ownr_ph1, "CA");
+ if (p && p.isValid()) {
+ // openChatByPhone({
+ // phone_num: p.formatInternational(),
+ // jobid: job.id,
+ // });
+ setMessage(
+ `${window.location.protocol}//${window.location.host}/csi/${result.data.insert_csi.returning[0].id}`
+ );
+ } else {
+ notification["error"]({
+ message: t("messaging.error.invalidphone"),
+ });
+ }
+ }
} else {
notification["error"]({
- message: t("csi.errors.creating", {
- message: JSON.stringify(result.errors),
- }),
+ message: t("csi.errors.notconfigured"),
});
- return;
}
+ } else {
if (e.key === "email")
setEmailOptions({
messageOptions: {
@@ -91,17 +138,27 @@ export function JobsDetailHeaderCsi({
template: {
name: TemplateList("job").csi_invitation.key,
variables: {
- id: result.data.insert_csi.returning[0].id,
+ id: job.csi_invites[0].id,
},
},
});
if (e.key === "text") {
+ const p = parsePhoneNumber(job.ownr_ph1, "CA");
+ if (p && p.isValid()) {
+ // openChatByPhone({
+ // phone_num: p.formatInternational(),
+ // jobid: job.id,
+ // });
+ setMessage(
+ `${window.location.protocol}//${window.location.host}/csi/${job.csi_invites[0].id}`
+ );
+ } else {
+ notification["error"]({
+ message: t("messaging.error.invalidphone"),
+ });
+ }
}
- } else {
- notification["error"]({
- message: t("csi.errors.notconfigured"),
- });
}
};
@@ -122,13 +179,26 @@ export function JobsDetailHeaderCsi({
{t("general.labels.text")}
- {job.csiinvites.map((item, idx) => (
-
-
- {item.completedon}
-
-
- ))}
+ {job.csiinvites.map((item, idx) => {
+ return item.completedon ? (
+
+
+ {item.completedon}
+
+
+ ) : (
+ {
+ navigator.clipboard.writeText(
+ `${window.location.protocol}//${window.location.host}/csi/${item.id}`
+ );
+ }}
+ >
+ {t("general.actions.copylink")}
+
+ );
+ })}
);
}
diff --git a/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx b/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx
index 46d4d1be7..62d7e6202 100644
--- a/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx
+++ b/client/src/components/jobs-detail-rates/jobs-detail-rates.parts.component.jsx
@@ -136,6 +136,46 @@ export function JobsDetailRatesParts({ jobRO, expanded, required = true }) {
+